Skip to content

Commit a1a0446

Browse files
committed
enhance: default SuperKMeans coarse quantizer training for SCANN
Enable SuperKMeans (super fast k-means) for SCANN coarse quantizer training by default. SuperKMeans is a faster k-means variant using ADSampling + PDX progressive pruning; on inner-product data its centroids are unit-normalized so L2 assignment is equivalent to IP argmax. Vendored faiss changes (tracked in thirdparty/faiss, mirrored to the upstream faiss PR): - SuperKMeans: spherical (inner-product) support via cp.spherical. Centroid renormalization makes L2 assignment equivalent to IP. Power-of-two d uses the fast HadamardRotation; L2 keeps RandomRotationMatrix. - ClusteringParameters::use_super_kmeans (default false) lets Level1Quantizer::train_q1 route coarse quantizer training through SuperKMeans. - Low-dimensional L2sqr nearest fast path (d in {2,4,8}) in fvec_L2sqr_ny_nearest<ARM_SVE>: one SVE lane per centroid, min index tracked in registers, scratch buffer not written (matching x86 D2/D4/D8 implementations). ProductQuantizer::compute_code switches to AVAILABLE_SIMD_LEVELS_A1 so PQ encoding reaches the SVE path. - HadamardRotation::reverse_transform (missing inverse). - block_l2<ARM_SVE> SVE kernel for the SuperKMeans pruning loop. Knowhere changes: - ScannConfig::use_super_kmeans defaults to true. - IvfConfig::use_super_kmeans plumbing into all IVF-family build paths. - New unit test tests/ut/test_scann_superkmeans.cc verifies SCANN with SuperKMeans coarse training is recall-equivalent to Clustering. Benchmark (qwen 4096-dim, IP, nlist=1024, sub_dim=4, 1 thread, 50k rows): - SCANN_DVR Train 67.73s -> 24.36s, Add 10.86s -> 7.17s, Build 78.59s -> 31.52s (-60%) - recall@10 unchanged vs Clustering baseline (diff <= 0.0002) Signed-off-by: marcelo-cjl <marcelo.chen@zilliz.com>
1 parent 03b845e commit a1a0446

18 files changed

Lines changed: 475 additions & 19 deletions

cmake/libs/libfaiss.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ knowhere_file_glob(
187187
FAISS_DD_SVE_SRCS
188188
thirdparty/faiss/faiss/impl/pq_code_distance/pq_code_distance-sve.cpp
189189
thirdparty/faiss/faiss/utils/simd_impl/distances_arm_sve.cpp
190+
thirdparty/faiss/faiss/utils/simd_impl/super_kmeans_kernels_sve.cpp
190191
)
191192
# combine files
192193
list(APPEND FAISS_SVE_SRCS ${FAISS_DD_SVE_SRCS})
@@ -548,6 +549,7 @@ if(__AARCH64)
548549
knowhere_utils)
549550
if(SVE_AVAILABLE)
550551
target_link_libraries(faiss PUBLIC faiss_sve)
552+
target_compile_definitions(faiss PRIVATE COMPILE_SIMD_ARM_SVE)
551553
endif()
552554
target_compile_definitions(faiss PRIVATE FINTEGER=int FAISS_ENABLE_DD COMPILE_SIMD_ARM_NEON)
553555
endif()

include/knowhere/comp/index_param.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ namespace indexparam {
145145
constexpr const char* NPROBE = "nprobe";
146146
constexpr const char* NLIST = "nlist";
147147
constexpr const char* USE_ELKAN = "use_elkan";
148+
constexpr const char* USE_SUPER_KMEANS = "use_super_kmeans";
148149
constexpr const char* NBITS = "nbits"; // PQ/SQ
149150
constexpr const char* M = "m"; // PQ param for IVFPQ
150151
constexpr const char* IVF_SQ_TYPE = "sq_type"; // SQ param for IVFSQ

src/index/ivf/ivf.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,7 @@ IvfIndexNode<DataType, IndexType>::TrainInternal(const DataSetPtr dataset, std::
599599
}
600600
// apply clustering config
601601
ApplyClusteringConfig(index->cp);
602+
index->cp.use_super_kmeans = ivf_flat_cfg.use_super_kmeans.value();
602603
// train
603604
index->train(rows, static_cast<const float*>(data));
604605
// transfer ownership of qzr to index
@@ -621,6 +622,7 @@ IvfIndexNode<DataType, IndexType>::TrainInternal(const DataSetPtr dataset, std::
621622
}
622623
// apply clustering config
623624
ApplyClusteringConfig(index->cp);
625+
index->cp.use_super_kmeans = ivf_flat_cc_cfg.use_super_kmeans.value();
624626
// train
625627
index->train(rows, static_cast<const float*>(data));
626628
// transfer ownership of qzr to index
@@ -652,6 +654,7 @@ IvfIndexNode<DataType, IndexType>::TrainInternal(const DataSetPtr dataset, std::
652654

653655
// apply clustering config
654656
ApplyClusteringConfig(index->get_base_ivf_index()->cp);
657+
index->get_base_ivf_index()->cp.use_super_kmeans = ivf_pq_cfg.use_super_kmeans.value();
655658
// train
656659
index->train(rows, static_cast<const float*>(data));
657660
// transfer ownership of qzr to index
@@ -678,6 +681,7 @@ IvfIndexNode<DataType, IndexType>::TrainInternal(const DataSetPtr dataset, std::
678681
}
679682
// apply clustering config
680683
ApplyClusteringConfig(base_index->cp);
684+
base_index->cp.use_super_kmeans = scann_cfg.use_super_kmeans.value();
681685
// create scann index, which does not base_index by default,
682686
// but owns the refine index by default omg
683687
if (scann_cfg.with_raw_data.value()) {
@@ -713,6 +717,7 @@ IvfIndexNode<DataType, IndexType>::TrainInternal(const DataSetPtr dataset, std::
713717

714718
// apply clustering config
715719
ApplyClusteringConfig(index->get_base_ivf_index()->cp);
720+
index->get_base_ivf_index()->cp.use_super_kmeans = ivf_sq_cfg.use_super_kmeans.value();
716721
// train
717722
index->train(rows, static_cast<const float*>(data));
718723
// transfer ownership of qzr to index
@@ -730,6 +735,7 @@ IvfIndexNode<DataType, IndexType>::TrainInternal(const DataSetPtr dataset, std::
730735
index = std::make_unique<faiss::cppcontrib::knowhere::IndexBinaryIVF>(qzr.get(), dim, nlist, metric.value());
731736
// apply clustering config
732737
ApplyClusteringConfig(index->cp);
738+
index->cp.use_super_kmeans = ivf_bin_cfg.use_super_kmeans.value();
733739
// train
734740
index->train(rows, static_cast<const uint8_t*>(data));
735741
// transfer ownership of qzr to index
@@ -760,6 +766,7 @@ IvfIndexNode<DataType, IndexType>::TrainInternal(const DataSetPtr dataset, std::
760766
}
761767
// apply clustering config
762768
ApplyClusteringConfig(index->cp);
769+
index->cp.use_super_kmeans = ivf_sq_cc_cfg.use_super_kmeans.value();
763770
// train
764771
index->train(rows, static_cast<const float*>(data));
765772
// transfer ownership of qzr to index
@@ -782,6 +789,7 @@ IvfIndexNode<DataType, IndexType>::TrainInternal(const DataSetPtr dataset, std::
782789
index = std::move(result.value());
783790
// apply clustering config
784791
ApplyClusteringConfig(index->get_ivfrabitq_index()->cp);
792+
index->get_ivfrabitq_index()->cp.use_super_kmeans = ivf_rabitq_cfg.use_super_kmeans.value();
785793
// train
786794
index->train(rows, static_cast<const float*>(data));
787795
}
@@ -798,6 +806,7 @@ IvfIndexNode<DataType, IndexType>::TrainInternal(const DataSetPtr dataset, std::
798806
auto* fs_idx = index->get_fastscan_index();
799807
if (fs_idx) {
800808
ApplyClusteringConfig(fs_idx->cp);
809+
fs_idx->cp.use_super_kmeans = fs_cfg.use_super_kmeans.value();
801810
}
802811
index->train(rows, static_cast<const float*>(data));
803812
}

src/index/ivf/ivf_config.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class IvfConfig : public BaseConfig {
2727
CFG_INT nlist;
2828
CFG_INT nprobe;
2929
CFG_BOOL use_elkan;
30+
CFG_BOOL use_super_kmeans;
3031
CFG_BOOL ensure_topk_full; // internal config, used for temp index
3132
CFG_INT max_empty_result_buckets;
3233
KNOWHERE_DECLARE_CONFIG(IvfConfig) {
@@ -46,6 +47,10 @@ class IvfConfig : public BaseConfig {
4647
.set_default(true)
4748
.description("whether to use elkan algorithm")
4849
.for_train();
50+
KNOWHERE_CONFIG_DECLARE_FIELD(use_super_kmeans)
51+
.set_default(false)
52+
.description("whether to use SuperKMeans for coarse quantizer training")
53+
.for_train();
4954
KNOWHERE_CONFIG_DECLARE_FIELD(ensure_topk_full)
5055
.set_default(true)
5156
.description("whether to make sure topk results full")
@@ -196,6 +201,13 @@ class ScannConfig : public IvfFlatConfig {
196201
.set_default(false)
197202
.description("whether to make sure topk results full")
198203
.for_search();
204+
// SCANN defaults to SuperKMeans for coarse quantizer training: the
205+
// super-fast k-means variant is recall-equivalent to Clustering on
206+
// inner-product data but trains significantly faster for large nlist.
207+
KNOWHERE_CONFIG_DECLARE_FIELD(use_super_kmeans)
208+
.set_default(true)
209+
.description("whether to use SuperKMeans for coarse quantizer training")
210+
.for_train();
199211
}
200212

201213
Status

tests/ut/test_cluster.cc

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "catch2/generators/catch_generators.hpp"
1919
#include "faiss/Clustering.h"
2020
#include "faiss/IndexFlat.h"
21+
#include "faiss/SuperKMeans.h"
2122
#include "faiss/cppcontrib/knowhere/utils/binary_distances.h"
2223
#include "hnswlib/hnswalg.h"
2324
#include "knowhere/bitsetview.h"
@@ -143,3 +144,48 @@ TEST_CASE("Test Kmeans With Float Vector", "[float metrics]") {
143144
REQUIRE(recall > kKnnRecallThreshold);
144145
}
145146
}
147+
148+
// SuperKMeans spherical (inner-product) support: unit-normalized centroids
149+
// make L2 assignment equivalent to IP argmax, so the final objective must
150+
// track vanilla spherical Clustering and centroids must be unit norm.
151+
TEST_CASE("Test SuperKMeans Spherical", "[cluster]") {
152+
const int d = 64;
153+
const int k = 16;
154+
const size_t n = 2000;
155+
156+
std::mt19937 rng(42);
157+
std::normal_distribution<float> dist(0.f, 1.f);
158+
std::vector<float> x(n * d);
159+
for (auto& v : x) {
160+
v = dist(rng);
161+
}
162+
163+
faiss::SuperKMeansParameters sp;
164+
sp.seed = 42;
165+
sp.niter = 10;
166+
sp.spherical = true;
167+
faiss::SuperKMeans sc(d, k, sp);
168+
sc.train(n, x.data());
169+
170+
// Centroids must be unit norm under spherical clustering.
171+
for (int j = 0; j < k; ++j) {
172+
float norm = 0.f;
173+
for (int i = 0; i < d; ++i) {
174+
norm += sc.centroids[j * d + i] * sc.centroids[j * d + i];
175+
}
176+
norm = std::sqrt(norm);
177+
REQUIRE(norm == Catch::Approx(1.f).margin(1e-4));
178+
}
179+
180+
// Final objective must track vanilla spherical Clustering.
181+
const float sc_final = sc.iteration_stats.at(sc.iteration_stats.size() - 1).obj;
182+
faiss::ClusteringParameters vp;
183+
vp.seed = 42;
184+
vp.niter = 10;
185+
vp.spherical = true;
186+
faiss::Clustering vanilla(d, k, vp);
187+
faiss::IndexFlatL2 quantizer(d);
188+
vanilla.train(n, x.data(), quantizer);
189+
const float v_final = vanilla.iteration_stats.at(vanilla.iteration_stats.size() - 1).obj;
190+
REQUIRE(std::abs(sc_final - v_final) / v_final < 0.05f);
191+
}

tests/ut/test_scann_superkmeans.cc

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (C) 2019-2023 Zilliz. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
4+
// with the License. You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software distributed under the License
9+
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
10+
// or implied. See the License for the specific language governing permissions and limitations under the License.
11+
12+
#include <string>
13+
#include <vector>
14+
15+
#include "catch2/catch_approx.hpp"
16+
#include "catch2/catch_test_macros.hpp"
17+
#include "knowhere/comp/brute_force.h"
18+
#include "knowhere/comp/index_param.h"
19+
#include "knowhere/index/index_factory.h"
20+
#include "knowhere/version.h"
21+
#include "utils.h"
22+
23+
namespace {
24+
25+
// Build a SCANN index over train_ds with the given use_super_kmeans value
26+
// and return recall@k against brute-force ground truth.
27+
float
28+
BuildScannAndRecall(
29+
const knowhere::DataSetPtr& train_ds,
30+
const knowhere::DataSetPtr& query_ds,
31+
int64_t nlist,
32+
int64_t nprobe,
33+
int64_t topk,
34+
bool use_super_kmeans) {
35+
const auto version = knowhere::Version::GetCurrentVersion().VersionNumber();
36+
auto idx = knowhere::IndexFactory::Instance()
37+
.Create<knowhere::fp32>(knowhere::IndexEnum::INDEX_FAISS_SCANN, version)
38+
.value();
39+
40+
knowhere::Json cfg;
41+
cfg[knowhere::meta::METRIC_TYPE] = knowhere::metric::IP;
42+
cfg[knowhere::indexparam::NLIST] = nlist;
43+
cfg[knowhere::indexparam::NPROBE] = nprobe;
44+
cfg[knowhere::indexparam::SUB_DIM] = 4;
45+
cfg[knowhere::indexparam::WITH_RAW_DATA] = false;
46+
cfg[knowhere::indexparam::USE_SUPER_KMEANS] = use_super_kmeans;
47+
48+
REQUIRE(idx.Build(train_ds, cfg) == knowhere::Status::success);
49+
50+
knowhere::Json search_cfg;
51+
search_cfg[knowhere::meta::METRIC_TYPE] = knowhere::metric::IP;
52+
search_cfg[knowhere::meta::TOPK] = topk;
53+
search_cfg[knowhere::indexparam::NPROBE] = nprobe;
54+
auto results = idx.Search(query_ds, search_cfg, nullptr);
55+
REQUIRE(results.has_value());
56+
57+
auto gt = knowhere::BruteForce::Search<knowhere::fp32>(
58+
train_ds, query_ds,
59+
knowhere::Json{{knowhere::meta::METRIC_TYPE, knowhere::metric::IP},
60+
{knowhere::meta::TOPK, topk}},
61+
nullptr);
62+
REQUIRE(gt.has_value());
63+
64+
return GetKNNRecall(*gt.value(), *results.value());
65+
}
66+
67+
} // namespace
68+
69+
TEST_CASE("SCANN use_super_kmeans default matches Clustering recall", "[scann]") {
70+
constexpr int64_t nb = 2000;
71+
constexpr int64_t nq = 100;
72+
constexpr int64_t dim = 64;
73+
constexpr int64_t topk = 10;
74+
constexpr int64_t nlist = 128;
75+
constexpr int64_t nprobe = 8;
76+
77+
const auto train_ds = GenDataSet(nb, dim, kSeed);
78+
const auto query_ds = GenDataSet(nq, dim, kSeed);
79+
80+
const float super_recall = BuildScannAndRecall(train_ds, query_ds, nlist, nprobe, topk, true);
81+
const float cluster_recall = BuildScannAndRecall(train_ds, query_ds, nlist, nprobe, topk, false);
82+
83+
CAPTURE(super_recall, cluster_recall);
84+
// SuperKMeans coarse quantizer training is recall-equivalent to Clustering.
85+
REQUIRE(super_recall == Catch::Approx(cluster_recall).margin(0.02f));
86+
// Sanity: recall should be reasonable for nprobe=8.
87+
REQUIRE(super_recall > 0.5f);
88+
}
89+
90+
TEST_CASE("SCANN use_super_kmeans field is honored", "[scann]") {
91+
// Explicitly disabled must build and search successfully too.
92+
constexpr int64_t nb = 1000;
93+
constexpr int64_t nq = 50;
94+
constexpr int64_t dim = 32;
95+
constexpr int64_t topk = 10;
96+
constexpr int64_t nlist = 64;
97+
constexpr int64_t nprobe = 4;
98+
99+
const auto train_ds = GenDataSet(nb, dim, kSeed + 1);
100+
const auto query_ds = GenDataSet(nq, dim, kSeed + 1);
101+
102+
const float cluster_recall = BuildScannAndRecall(train_ds, query_ds, nlist, nprobe, topk, false);
103+
REQUIRE(cluster_recall > 0.0f);
104+
}

thirdparty/faiss/faiss/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ set(FAISS_SIMD_NEON_SRC
6464
set(FAISS_SIMD_SVE_SRC
6565
impl/pq_code_distance/pq_code_distance-sve.cpp
6666
utils/simd_impl/distances_arm_sve.cpp
67+
utils/simd_impl/super_kmeans_kernels_sve.cpp
6768
)
6869
set(FAISS_SIMD_RVV_SRC
6970
impl/fast_scan/impl-riscv.cpp

thirdparty/faiss/faiss/Clustering.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ struct ClusteringParameters {
7575
/// so the training process stops only if an error
7676
/// is unchanged from the previous iteration.
7777
double early_stop_threshold = 0.0;
78+
79+
/// Whether to use the SuperKMeans (super fast k-means) variant instead of
80+
/// the vanilla Clustering implementation. Only honored by callers that
81+
/// explicitly support it (e.g. IVF level-1 quantizer training).
82+
bool use_super_kmeans = false;
7883
};
7984

8085
struct ClusteringIterationStats {

thirdparty/faiss/faiss/IndexIVF.cpp

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
#include <faiss/IndexIVF.h>
9+
#include <faiss/SuperKMeans.h>
910

1011
#include <omp.h>
1112
#include <atomic>
@@ -78,13 +79,22 @@ void Level1Quantizer::train_q1(
7879
printf("Training level-1 quantizer on %zd vectors in %zdD\n", n, d);
7980
}
8081

81-
Clustering clus(static_cast<int>(d), static_cast<int>(nlist), cp);
8282
quantizer->reset();
83-
if (clustering_index) {
84-
clus.train(n, x, *clustering_index);
83+
if (cp.use_super_kmeans && clustering_index == nullptr) {
84+
SuperKMeansParameters super_cp;
85+
static_cast<ClusteringParameters&>(super_cp) = cp;
86+
SuperKMeans clus(
87+
static_cast<int>(d), static_cast<int>(nlist), super_cp);
88+
clus.train(n, x);
8589
quantizer->add(nlist, clus.centroids.data());
8690
} else {
87-
clus.train(n, x, *quantizer);
91+
Clustering clus(static_cast<int>(d), static_cast<int>(nlist), cp);
92+
if (clustering_index) {
93+
clus.train(n, x, *clustering_index);
94+
quantizer->add(nlist, clus.centroids.data());
95+
} else {
96+
clus.train(n, x, *quantizer);
97+
}
8898
}
8999
quantizer->is_trained = true;
90100
} else if (quantizer_trains_alone == 2) {

0 commit comments

Comments
 (0)