From 76c67b7b736ca80c2b3ea29528434d6583adc88b Mon Sep 17 00:00:00 2001 From: Akash Nayar <33558235+aknayar@users.noreply.github.com> Date: Mon, 3 Aug 2026 10:53:18 -0700 Subject: [PATCH] Update default Panorama batch size to 1024 (#5441) Summary: ## Changes This PR unifies the default Panorama batch size to 1024 going forward. This was shown to offer better performance than the current values ([paper](https://arxiv.org/abs/2510.00566), figure 11). ## Performance ### IVFFlatPanorama SIFT1M | nprobe | qps_before | qps_after | speedup | |---|---|---|---| | 1 | 5980.4 | 6343.7 | 1.06x | | 2 | 3537.7 | 3933.2 | 1.11x | | 4 | 2105.4 | 2419.9 | 1.15x | | 8 | 1245.2 | 1466.4 | 1.18x | | 16 | 729.2 | 887.4 | 1.22x | | 32 | 429.6 | 538.0 | 1.25x | | 64 | 249.0 | 319.3 | 1.28x | GIST1M | nprobe | qps_before | qps_after | speedup | |---|---|---|---| | 1 | 1826.0 | 1494.7 | 0.82x | | 2 | 1091.0 | 1001.7 | 0.92x | | 4 | 642.2 | 639.0 | 0.99x | | 8 | 368.7 | 392.7 | 1.07x | | 16 | 212.2 | 233.9 | 1.10x | | 32 | 122.2 | 138.6 | 1.13x | | 64 | 71.3 | 82.6 | 1.16x | ## FlatL2Panorama SIFT1M | qps_before | qps_after | speedup | |---|---|---| | 124.8 | 133.4 | 1.07x | GIST1M | qps_before | qps_after | speedup | |---|---|---| | 44.1 | 46.2 | 1.05x | Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5441 Reviewed By: alibeklfc Differential Revision: D114387063 Pulled By: mnorris11 fbshipit-source-id: b71ba6ced074c4998aee3183df236b692626cd34 --- benchs/bench_flat_l2_panorama.py | 3 +- benchs/bench_ivf_flat_panorama.py | 2 +- faiss/IndexFlat.h | 4 +- faiss/impl/Panorama.h | 3 +- faiss/impl/index_read.cpp | 4 +- faiss/impl/index_write.cpp | 37 ++++++------------ faiss/index_factory.cpp | 2 +- tests/test_ivf_flat_panorama.py | 64 ++++++++++++++++++++++++++++++- 8 files changed, 82 insertions(+), 37 deletions(-) diff --git a/benchs/bench_flat_l2_panorama.py b/benchs/bench_flat_l2_panorama.py index 034ba1ddb..3d274c44e 100644 --- a/benchs/bench_flat_l2_panorama.py +++ b/benchs/bench_flat_l2_panorama.py @@ -69,13 +69,12 @@ def build_index(name): nlevels = 16 if args.dataset == "gist1m" else 8 -batch_size = 512 plt.figure(figsize=(8, 6), dpi=80) names = [ "Flat", - f"PCA{d},FlatL2Panorama{nlevels}_{batch_size}", + f"PCA{d},FlatL2Panorama{nlevels}", ] labels = [] diff --git a/benchs/bench_ivf_flat_panorama.py b/benchs/bench_ivf_flat_panorama.py index 4c5fe9687..725842962 100644 --- a/benchs/bench_ivf_flat_panorama.py +++ b/benchs/bench_ivf_flat_panorama.py @@ -98,7 +98,7 @@ plt.figure(figsize=(8, 6), dpi=80) eval_and_plot(f"IVF{nlist},Flat") # IVFFlatPanorama (with PCA transform to concentrate energy in early dimensions) -eval_and_plot(f"PCA{d},IVF{nlist},FlatPanorama{nlevels}_{1024}") +eval_and_plot(f"PCA{d},IVF{nlist},FlatPanorama{nlevels}") dataset_label = args.dataset.upper() plt.title(f"IVF Flat Indexes on {dataset_label}") diff --git a/faiss/IndexFlat.h b/faiss/IndexFlat.h index 4cdc7d438..7c8fb889f 100644 --- a/faiss/IndexFlat.h +++ b/faiss/IndexFlat.h @@ -176,7 +176,7 @@ struct IndexFlatL2Panorama : IndexFlatPanorama { explicit IndexFlatL2Panorama( idx_t d_in, size_t n_levels_in, - size_t batch_size_in = 512) + size_t batch_size_in = Panorama::kDefaultBatchSize) : IndexFlatPanorama(d_in, METRIC_L2, n_levels_in, batch_size_in) {} }; @@ -189,7 +189,7 @@ struct IndexFlatIPPanorama : IndexFlatPanorama { explicit IndexFlatIPPanorama( idx_t d_in, size_t n_levels_in, - size_t batch_size_in = 512) + size_t batch_size_in = Panorama::kDefaultBatchSize) : IndexFlatPanorama( d_in, METRIC_INNER_PRODUCT, diff --git a/faiss/impl/Panorama.h b/faiss/impl/Panorama.h index 090b51099..6374db12a 100644 --- a/faiss/impl/Panorama.h +++ b/faiss/impl/Panorama.h @@ -235,7 +235,8 @@ inline auto with_bool(bool value, Lambda&& fn) { * accelerating the refinement stage. */ struct Panorama { - static constexpr size_t kDefaultBatchSize = 128; + static constexpr size_t kDefaultBatchSize = 1024; + static constexpr size_t kLegacyBatchSize = 128; size_t d = 0; size_t code_size = 0; diff --git a/faiss/impl/index_read.cpp b/faiss/impl/index_read.cpp index 66d875ff4..29151ef50 100644 --- a/faiss/impl/index_read.cpp +++ b/faiss/impl/index_read.cpp @@ -545,7 +545,7 @@ std::unique_ptr read_InvertedLists_up( READ1(n_levels); FAISS_THROW_IF_NOT_FMT( n_levels > 0, "invalid ilpn n_levels %zd", n_levels); - constexpr size_t bs = Panorama::kDefaultBatchSize; + constexpr size_t bs = Panorama::kLegacyBatchSize; auto ailp = std::make_unique( nlist, code_size, n_levels, bs); std::vector sizes(nlist); @@ -2136,7 +2136,7 @@ std::unique_ptr read_index_up(IOReader* f, int io_flags) { read_ivf_header(ivfp.get(), f); ivfp->code_size = ivfp->d * sizeof(float); READ1(ivfp->n_levels); - ivfp->batch_size = Panorama::kDefaultBatchSize; + ivfp->batch_size = Panorama::kLegacyBatchSize; read_InvertedLists(*ivfp, f, io_flags); idx = std::move(ivfp); } else if (h == fourcc("IwP2")) { diff --git a/faiss/impl/index_write.cpp b/faiss/impl/index_write.cpp index 63892f02d..873fc0f9c 100644 --- a/faiss/impl/index_write.cpp +++ b/faiss/impl/index_write.cpp @@ -275,20 +275,12 @@ void write_InvertedLists(const InvertedLists* ils, IOWriter* f) { } else if ( const auto& ailp = dynamic_cast(ils)) { - if (ailp->pano.batch_size == Panorama::kDefaultBatchSize) { - uint32_t h = fourcc("ilpn"); - WRITE1(h); - WRITE1(ailp->nlist); - WRITE1(ailp->code_size); - WRITE1(ailp->n_levels); - } else { - uint32_t h = fourcc("ilp2"); - WRITE1(h); - WRITE1(ailp->nlist); - WRITE1(ailp->code_size); - WRITE1(ailp->n_levels); - WRITE1(ailp->pano.batch_size); - } + uint32_t h = fourcc("ilp2"); + WRITE1(h); + WRITE1(ailp->nlist); + WRITE1(ailp->code_size); + WRITE1(ailp->n_levels); + WRITE1(ailp->pano.batch_size); uint32_t list_type = fourcc("full"); WRITE1(list_type); std::vector sizes; @@ -736,18 +728,11 @@ void write_index(const Index* idx, IOWriter* f, int io_flags) { } else if ( const IndexIVFFlatPanorama* ivfp = dynamic_cast(idx)) { - if (ivfp->batch_size == Panorama::kDefaultBatchSize) { - uint32_t h = fourcc("IwPn"); - WRITE1(h); - write_ivf_header(ivfp, f); - WRITE1(ivfp->n_levels); - } else { - uint32_t h = fourcc("IwP2"); - WRITE1(h); - write_ivf_header(ivfp, f); - WRITE1(ivfp->n_levels); - WRITE1(ivfp->batch_size); - } + uint32_t h = fourcc("IwP2"); + WRITE1(h); + write_ivf_header(ivfp, f); + WRITE1(ivfp->n_levels); + WRITE1(ivfp->batch_size); write_InvertedLists(ivfp->invlists, f); } else if ( const IndexIVFFlat* ivfl_2 = diff --git a/faiss/index_factory.cpp b/faiss/index_factory.cpp index 6d55e8623..73d1517bb 100644 --- a/faiss/index_factory.cpp +++ b/faiss/index_factory.cpp @@ -385,7 +385,7 @@ IndexIVF* parse_IndexIVF( } if (match("FlatPanorama([0-9]+)?(_([0-9]+))?")) { int nlevels = mres_to_int(sm[1], 8); // default to 8 levels - int bs = mres_to_int(sm[3], 128); + int bs = mres_to_int(sm[3], Panorama::kDefaultBatchSize); return new IndexIVFFlatPanorama( get_q(), d, nlist, nlevels, mt, own_il, bs); } diff --git a/tests/test_ivf_flat_panorama.py b/tests/test_ivf_flat_panorama.py index 89d871944..72d5ee244 100644 --- a/tests/test_ivf_flat_panorama.py +++ b/tests/test_ivf_flat_panorama.py @@ -71,6 +71,7 @@ class TestIndexIVFFlatPanorama(unittest.TestCase): nprobe=None, make_direct_map=False, metric=faiss.METRIC_L2, + batch_size=faiss.Panorama.kDefaultBatchSize, ): """Create and initialize IndexIVFFlatPanorama.""" quantizer = ( @@ -78,7 +79,9 @@ class TestIndexIVFFlatPanorama(unittest.TestCase): if metric == faiss.METRIC_L2 else faiss.IndexFlatIP(d) ) - index = faiss.IndexIVFFlatPanorama(quantizer, d, nlist, nlevels, metric) + index = faiss.IndexIVFFlatPanorama( + quantizer, d, nlist, nlevels, metric, True, batch_size + ) index.train(xt) if make_direct_map: index.make_direct_map() @@ -769,6 +772,56 @@ class TestIndexIVFFlatPanorama(unittest.TestCase): np.testing.assert_array_equal(I_before, I_after) np.testing.assert_array_equal(D_before, D_after) + def test_read_legacy_format(self): + """Indexes serialized in the legacy "IwPn"/"ilpn" format (which does + not store batch_size and implies the legacy value of 128) must keep + deserializing correctly, whatever the current default batch_size is. + + The current code always writes the explicit-batch_size format + ("IwP2"/"ilp2"), so a legacy stream is reconstructed here by + transforming a serialized index: swap the fourccs and drop the two + 8-byte batch_size fields. + """ + d, nlist, nlevels, nb, nq, k = 32, 4, 8, 2000, 10, 10 + legacy_bs = 128 + rng = np.random.RandomState(123) + xb = rng.rand(nb, d).astype("float32") + xq = rng.rand(nq, d).astype("float32") + + quantizer = faiss.IndexFlatL2(d) + index = faiss.IndexIVFFlatPanorama( + quantizer, d, nlist, nlevels, faiss.METRIC_L2, True, legacy_bs + ) + index.train(xb) + index.add(xb) + index.nprobe = nlist + D_ref, I_ref = index.search(xq, k) + + buf = faiss.serialize_index(index).tobytes() + # layout: "IwP2" | ivf header | n_levels (8) | batch_size (8) | + # "ilp2" | nlist (8) | code_size (8) | n_levels (8) | + # batch_size (8) | inverted lists data + self.assertEqual(buf[:4], b"IwP2") + self.assertEqual(buf.count(b"ilp2"), 1) + p = buf.index(b"ilp2") + legacy = ( + b"IwPn" + + buf[4 : p - 8] # drop the IVF-level batch_size field + + b"ilpn" + + buf[p + 4 : p + 28] # nlist, code_size, n_levels + + buf[p + 36 :] # drop the invlist-level batch_size field + ) + + index_legacy = faiss.deserialize_index( + np.frombuffer(legacy, dtype=np.uint8) + ) + self.assertIsInstance(index_legacy, faiss.IndexIVFFlatPanorama) + self.assertEqual(index_legacy.batch_size, legacy_bs) + index_legacy.nprobe = nlist + D_legacy, I_legacy = index_legacy.search(xq, k) + np.testing.assert_array_equal(I_ref, I_legacy) + np.testing.assert_array_equal(D_ref, D_legacy) + def test_ratio_dims_scanned(self): """Test the correctness of the ratio of dimensions scanned""" d, nb, nq, nlist, k = 128, 500000, 1, 1, 1 @@ -798,7 +851,14 @@ class TestIndexIVFFlatPanorama(unittest.TestCase): with self.subTest(nlevels=nlevels): faiss.cvar.indexPanorama_stats.reset() index = self.create_panorama( - d, nlist, nlevels, xt, xb, nprobe=1, metric=metric + d, + nlist, + nlevels, + xt, + xb, + nprobe=1, + metric=metric, + batch_size=128, ) D, I = index.search(xq, k) self.assert_search_results_equal(D_base, I_base, D, I)