Skip to content

Commit 5d1c5ed

Browse files
committed
Add query blocking to IndexFlatPanorama search
Process queries in blocks so each DB level-storage block is streamed from memory once and reused across the block while resident in a faster cache level, raising cache-bandwidth efficiency on the bandwidth-bound Panorama dot kernel. The DB-batch loop stays outside the block (preserving each query's threshold evolution) and within a batch the loop runs level-outer / query-inner. Controlled by the global panorama_query_block_size, following the existing FAISS tunable idiom (cf. distance_compute_blas_query_bs). It defaults to an enabled value of 32; 0 or 1 selects the original query-at-a-time path. Pure loop-order transform: each query keeps its own active set, threshold, and pruning decisions, so results are bit-identical to the original path (verified 432/432 equality cases + unchanged SIFT1M recall). Top-k only; range search falls back to the original path since RangeSearchPartialResult requires per-query-contiguous appends. Speedup up to ~2.8x (n_levels=4) on SIFT1M 1M-vec single core; holds multithreaded by relieving shared-cache contention. Also add --query-block-size (comma-separated sweep) and --nq to bench_flat_l2_panorama so the speedup can be measured directly: the Panorama index is built once and only the block-size global is toggled between runs, printing speedups vs plain Flat and vs the Panorama baseline. Signed-off-by: Mulugeta Mammo <mulugeta.mammo@intel.com>
1 parent 1f755ef commit 5d1c5ed

5 files changed

Lines changed: 407 additions & 20 deletions

File tree

benchs/bench_flat_l2_panorama.py

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,28 @@
1818

1919
parser = argparse.ArgumentParser()
2020
parser.add_argument("--dataset", default="gist1m", choices=["sift1m", "gist1m"])
21+
parser.add_argument(
22+
"--nq",
23+
type=int,
24+
default=1000,
25+
help="number of queries (query blocking needs a real batch to help)",
26+
)
27+
parser.add_argument(
28+
"--query-block-size",
29+
default="0",
30+
help="comma-separated block sizes to sweep for the Panorama index; "
31+
"0 or 1 selects the original query-at-a-time path",
32+
)
2133
args = parser.parse_args()
2234

35+
query_block_sizes = [int(v) for v in args.query_block_size.split(",")]
36+
2337
if args.dataset == "sift1m":
2438
ds = DatasetSIFT1M()
2539
else:
2640
ds = DatasetGIST1M()
2741

28-
nq = 10
42+
nq = args.nq
2943
xq = ds.get_queries()[:nq]
3044
xb = ds.get_database()
3145
gt = ds.get_groundtruth()[:nq]
@@ -73,32 +87,53 @@ def build_index(name):
7387

7488
plt.figure(figsize=(8, 6), dpi=80)
7589

76-
names = [
77-
"Flat",
78-
f"PCA{d},FlatL2Panorama{nlevels}_{batch_size}",
79-
]
90+
pano_name = f"PCA{d},FlatL2Panorama{nlevels}_{batch_size}"
8091

8192
labels = []
8293
qps_values = []
8394

84-
for name in names:
85-
print(f"======{name}")
86-
index = build_index(name)
87-
recall, qps = eval_qps(index)
88-
labels.append(f"{name}\n(r@{recall:.3f})")
95+
# Plain Flat baseline.
96+
print("======Flat")
97+
flat_index = build_index("Flat")
98+
flat_recall, flat_qps = eval_qps(flat_index)
99+
labels.append(f"Flat\n(r@{flat_recall:.3f})")
100+
qps_values.append(flat_qps)
101+
102+
# Panorama index, swept over query block sizes. The index is built once; only
103+
# the global query-block toggle changes between runs, so results must match.
104+
print(f"======{pano_name}")
105+
pano_index = build_index(pano_name)
106+
pano_qps_values = []
107+
for qbs in query_block_sizes:
108+
faiss.cvar.panorama_query_block_size = qbs
109+
tag = "baseline" if qbs <= 1 else f"block={qbs}"
110+
print(f"---{tag}")
111+
recall, qps = eval_qps(pano_index)
112+
labels.append(f"Pano\n{tag}\n(r@{recall:.3f})")
89113
qps_values.append(qps)
114+
pano_qps_values.append(qps)
115+
faiss.cvar.panorama_query_block_size = 0
116+
117+
# Report speedups: Panorama-vs-Flat, and blocked-vs-baseline-Panorama.
118+
pano_baseline_qps = pano_qps_values[0]
119+
print("\n=== summary ===")
120+
print(f"Flat: {flat_qps:.1f} QPS")
121+
for qbs, qps in zip(query_block_sizes, pano_qps_values):
122+
tag = "baseline" if qbs <= 1 else f"block={qbs}"
123+
vs_flat = qps / flat_qps
124+
vs_base = qps / pano_baseline_qps
125+
print(
126+
f"Pano {tag:>10}: {qps:8.1f} QPS "
127+
f"({vs_flat:.2f}x vs Flat, {vs_base:.2f}x vs Pano baseline)"
128+
)
90129

91-
x = np.arange(len(names))
92-
plt.bar(x, qps_values, color=["#1f77b4", "#ff7f0e"])
93-
speedup = qps_values[1] / qps_values[0]
130+
x = np.arange(len(qps_values))
131+
colors = ["#1f77b4"] + ["#ff7f0e"] * len(pano_qps_values)
132+
plt.bar(x, qps_values, color=colors)
94133
ax = plt.gca()
95-
ax.text(
96-
x[1],
97-
qps_values[1] * 1.01,
98-
f"{speedup:.2f}x",
99-
ha="center",
100-
va="bottom",
101-
)
134+
# Annotate each Panorama bar with its speedup over plain Flat.
135+
for xi, qps in zip(x[1:], pano_qps_values):
136+
ax.text(xi, qps * 1.01, f"{qps / flat_qps:.2f}x", ha="center", va="bottom")
102137
plt.xticks(x, labels, rotation=0)
103138
plt.ylabel("QPS")
104139
dataset_label = args.dataset.upper()

faiss/IndexFlat.cpp

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
namespace faiss {
2525

26+
size_t panorama_query_block_size = 32;
27+
2628
IndexFlat::IndexFlat(idx_t d_, MetricType metric)
2729
: IndexFlatCodes(sizeof(float) * d_, d_, metric) {}
2830

@@ -571,6 +573,120 @@ inline auto dispatch_metric_compare(MetricType metric, Fn&& fn) {
571573
return fn.template operator()<C>();
572574
}
573575

576+
/// Query-blocked search core. For each block of up to `qbs` queries, iterates
577+
/// DB batches in order (preserving each query's threshold evolution), and
578+
/// within each batch iterates level-outer / query-inner so the level storage
579+
/// block is streamed from memory once and reused across the block while
580+
/// resident in a faster cache level. Produces results identical to
581+
/// flat_pano_search_core.
582+
template <bool use_radius, typename C, typename BlockHandler>
583+
inline void flat_pano_search_core_blocked(
584+
const IndexFlatPanorama& index,
585+
BlockHandler& handler,
586+
idx_t n,
587+
const float* x,
588+
float radius,
589+
const SearchParameters* params,
590+
size_t qbs) {
591+
using SingleResultHandler = typename BlockHandler::SingleResultHandler;
592+
593+
IDSelector* sel = params ? params->sel : nullptr;
594+
bool use_sel = sel != nullptr;
595+
596+
const size_t d = index.d;
597+
const size_t bs = index.batch_size;
598+
const size_t n_lp1 = index.pano.n_levels + 1;
599+
size_t n_batches = (index.ntotal + bs - 1) / bs;
600+
601+
size_t n_blocks = (size_t(n) + qbs - 1) / qbs;
602+
[[maybe_unused]] int nt = std::min(int(n_blocks), omp_get_max_threads());
603+
604+
#pragma omp parallel num_threads(nt)
605+
{
606+
// One persistent SingleResultHandler per query slot so each heap and
607+
// threshold survive across DB batches (begin() re-heapifies, so it must
608+
// be called exactly once per query, not once per batch).
609+
std::vector<SingleResultHandler> ress;
610+
ress.reserve(qbs);
611+
for (size_t qb = 0; qb < qbs; qb++) {
612+
ress.emplace_back(handler);
613+
}
614+
615+
std::vector<float> query_cum_norms(qbs * n_lp1);
616+
std::vector<uint32_t> active_indices(qbs * bs);
617+
std::vector<uint8_t> active_byteset(qbs * bs);
618+
std::vector<float> exact_distances(qbs * bs);
619+
std::vector<float> dot_buffer(bs);
620+
std::vector<float> thresholds(qbs);
621+
std::vector<size_t> num_active(qbs);
622+
623+
#pragma omp for
624+
for (int64_t blk = 0; blk < int64_t(n_blocks); blk++) {
625+
size_t q0 = size_t(blk) * qbs;
626+
size_t block_size = std::min(qbs, size_t(n) - q0);
627+
628+
PanoramaStats local_stats;
629+
local_stats.reset();
630+
631+
// Per-query: compute cum sums and open the result heap (once).
632+
for (size_t qb = 0; qb < block_size; qb++) {
633+
const float* xi = x + (q0 + qb) * d;
634+
index.pano.compute_query_cum_sums(
635+
xi, query_cum_norms.data() + qb * n_lp1);
636+
ress[qb].begin(q0 + qb);
637+
}
638+
639+
for (size_t batch_no = 0; batch_no < n_batches; batch_no++) {
640+
size_t batch_start = batch_no * bs;
641+
642+
// Snapshot each query's current threshold for this batch.
643+
for (size_t qb = 0; qb < block_size; qb++) {
644+
if constexpr (use_radius) {
645+
thresholds[qb] = radius;
646+
} else {
647+
thresholds[qb] = ress[qb].threshold;
648+
}
649+
}
650+
651+
with_metric_type(index.metric_type, [&]<MetricType M>() {
652+
index.pano.progressive_filter_block<C, M>(
653+
index.codes.data(),
654+
index.cum_sums.data(),
655+
x + q0 * d,
656+
query_cum_norms.data(),
657+
block_size,
658+
batch_no,
659+
index.ntotal,
660+
sel,
661+
nullptr,
662+
use_sel,
663+
active_indices.data(),
664+
active_byteset.data(),
665+
exact_distances.data(),
666+
dot_buffer.data(),
667+
thresholds.data(),
668+
num_active.data(),
669+
local_stats);
670+
});
671+
672+
// Push survivors into each query's heap (updates threshold).
673+
for (size_t qb = 0; qb < block_size; qb++) {
674+
const uint32_t* ai = active_indices.data() + qb * bs;
675+
const float* ed = exact_distances.data() + qb * bs;
676+
for (size_t j = 0; j < num_active[qb]; j++) {
677+
ress[qb].add_result(ed[ai[j]], batch_start + ai[j]);
678+
}
679+
}
680+
}
681+
682+
for (size_t qb = 0; qb < block_size; qb++) {
683+
ress[qb].end();
684+
}
685+
indexPanorama_stats.add(local_stats);
686+
}
687+
}
688+
}
689+
574690
template <bool use_radius, typename C, typename BlockHandler>
575691
inline void flat_pano_search_core(
576692
const IndexFlatPanorama& index,
@@ -579,6 +695,17 @@ inline void flat_pano_search_core(
579695
const float* x,
580696
float radius,
581697
const SearchParameters* params) {
698+
// Query blocking is implemented for top-k search only. Range search uses a
699+
// RangeSearchPartialResult that finalizes once per handler and requires
700+
// per-query-contiguous appends, which is incompatible with the interleaved
701+
// block schedule; it falls through to the original path.
702+
size_t qbs = panorama_query_block_size;
703+
if (qbs > 1 && !use_radius) {
704+
flat_pano_search_core_blocked<use_radius, C>(
705+
index, handler, n, x, radius, params, qbs);
706+
return;
707+
}
708+
582709
using SingleResultHandler = typename BlockHandler::SingleResultHandler;
583710

584711
IDSelector* sel = params ? params->sel : nullptr;

faiss/IndexFlat.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@
1717

1818
namespace faiss {
1919

20+
/// Query-block size for IndexFlatPanorama search (default 32). When > 1,
21+
/// queries are processed in blocks so each DB level-block is loaded from memory
22+
/// once and reused across the block (raising cache-bandwidth efficiency). 0 or
23+
/// 1 selects the original query-at-a-time path. Results are identical for any
24+
/// value; this only trades off cache behavior. Set before searching; changing
25+
/// it concurrently with in-flight searches is not thread-safe.
26+
FAISS_API extern size_t panorama_query_block_size;
27+
2028
/** Index that stores the full vectors and performs exhaustive search */
2129
struct IndexFlat : IndexFlatCodes {
2230
explicit IndexFlat(

0 commit comments

Comments
 (0)