Clustering¶
RaBitQ Library provides two implementations of Lloyd's k-means. Both compute centroid means from the original float32 data, accumulate in float64, and require no external clustering package. Choose the class explicitly; neither switches algorithms based on the cluster count.
Both classes require RaBitQ Library 0.5.0 or newer; see installation options.
Choose a method¶
| Class | Assignment | Recommended use | Cluster count |
|---|---|---|---|
| RaBitQKMeans | Flat RaBitQ scan | Small cluster counts | n >= k >= 1 |
| QGKMeans | SymphonyQG centroid graph | Larger cluster counts | n >= k > graph_degree (default 32) |
Python¶
Both classes have the same training and result interface:
from rabitqlib import QGKMeans, RaBitQKMeans
kmeans = RaBitQKMeans(d, 16, niter=25, num_threads=8)
# For larger cluster counts, use instead:
# kmeans = QGKMeans(d, 4096, niter=25, num_threads=8)
last_iteration_obj = kmeans.train(x)
centroids = kmeans.centroids # shape (k, d)
assignments = kmeans.assignments # shape (n,)
distances = kmeans.distances # shape (n,)
final_obj = kmeans.final_obj
x has shape (n, d) and is converted to a C-contiguous float32 array.
C++¶
#include <rabitqlib/clustering/rabitqkmeans.hpp>
#include <rabitqlib/clustering/qgkmeans.hpp>
rabitqlib::rabitqkmeans::RaBitQKMeansParameters parameters;
parameters.niter = 25;
parameters.num_threads = 8;
rabitqlib::rabitqkmeans::RaBitQKMeans kmeans(dimension, 16, parameters);
// For graph assignment, use qgkmeans::QGKMeansParameters and qgkmeans::QGKMeans.
kmeans.train(num_points, data); // row-major float32 input
const auto& centroids = kmeans.centroids;
const auto& assignments = kmeans.assignments;
const auto& distances = kmeans.distances;
const double final_obj = kmeans.final_obj;
Complete examples: RaBitQKMeans
(bin/rabitqkmeans) and QGKMeans
(bin/qgkmeans). Run either without arguments for a synthetic demo, or save
clusters for the C++ indexing examples:
./bin/qgkmeans data.fvecs 4096 centroids.fvecs labels.ivecs
./bin/rabitqkmeans data.fvecs 16 centroids.fvecs labels.ivecs
File mode uses exact final assignment and the default training parameters.
Append ip for spherical clustering of normalized input; the default is l2.
example.sh runs the C++ clustering and indexing workflow without Python.
Parameters and input¶
Set Python keyword arguments or fields of the corresponding C++ parameter type:
| Parameter | Default | Meaning |
|---|---|---|
niter |
25 |
Maximum clustering iterations. |
early_stop_threshold |
0.0 |
Stop from iteration 2 when the absolute relative objective change is at most this value; range [0, 1]. Zero stops only on an unchanged objective. |
num_threads |
0 |
Use detected available logical CPUs; positive requests are capped at that count. See threading. |
spherical |
false |
Normalize centroids and assign by inner product; input must already be normalized. |
seed |
42 |
Seed for centroid selection and rotation. |
min_points_per_centroid |
39 |
Warn below this training-points-per-centroid ratio when verbose. |
verbose |
false |
Print training progress and warnings. |
final_assignment |
Approximate |
Use the selected method for final labels, or Exact for nearest-centroid labels. |
num_threads sets an upper limit, not a fixed worker count for every stage.
QGKMeans may use fewer workers to build small centroid graphs. That additional
limit does not apply to training-vector assignment, which uses the thread budget
after capping it to the available CPUs.
Training requires 64 <= d <= 65536, finite coordinates, and
abs(x) <= sqrt(sqrt(FLT_MAX) / (64 * d)); scale larger inputs before training.
Cluster-count requirements are listed above. Training does not normalize or
mutate the input; provide normalized vectors for spherical mode.
Empty clusters are reseeded from distant points; duplicate data can still leave empty clusters in the final assignment. Thread scheduling can affect graph construction and floating-point reductions, so a fixed seed does not guarantee identical multithreaded results.
Method details¶
RaBitQKMeans¶
The flat assignment design is inspired by SuperKMeans. Training points are rotated and encoded once around a fixed dataset mean. Each centroid becomes a query that scans all packed point batches, and each point receives the centroid with the best estimated distance.
Each iteration compares all n * k point-centroid pairs. The batch size of 32
is not a cluster-count limit. This method uses one-bit point codes throughout;
it has no graph parameters or quantization_bits setting.
QGKMeans¶
QGKMeans builds a SymphonyQG graph over the centroids and uses previous assignments as search hints. Its additional parameters are:
| Parameter | Default | Meaning |
|---|---|---|
quantization_bits |
0 |
Graph centroid storage: raw, or 4/8 bits. |
graph_degree |
32 |
A positive multiple of 32, strictly smaller than k. |
ef_build, ef_search |
240, 16 |
Positive graph construction and search window sizes. |
graph_build_iterations |
1 |
PiPNN initialization followed by one refinement; values ≥2 request that many graph passes, ending in refinement. Independent of niter. |
The seed also controls fallback neighbors; PiPNN uses fixed internal seeds.
QGAssigner in qgkmeans.hpp supports repeated vector-to-centroid assignment
without the clustering loop.
Results and final assignment¶
Python result properties are None until a successful fit; a failed fit
preserves the previous successful results.
distances contains distances to the returned assigned centroids, computed
from the original float32 vectors and centroids. For squared L2, final_obj is
their sum (WCSS); spherical mode sums 1 - inner_product. obj and
iteration_stats record objectives before each centroid update; Python
train() returns the last such objective.
FinalAssignmentMode.Approximate (default) uses the selected class's assignment
algorithm. The legacy name FinalAssignmentMode.SymphonyQG remains an alias for
Approximate. Labels need not identify the exact nearest centroids, but reported
distances and WCSS remain exact for those labels. Quantized QGKMeans final
assignment retains a previous label when it is closer. Approximate training
objectives need not decrease monotonically.
To require exact nearest-centroid final assignments with either class:
from rabitqlib import FinalAssignmentMode, RaBitQKMeans
kmeans = RaBitQKMeans(d, 16, final_assignment=FinalAssignmentMode.Exact)
kmeans.train(x)
In C++, set parameters.final_assignment to
rabitqlib::rabitqkmeans::FinalAssignmentMode::Exact or
rabitqlib::qgkmeans::FinalAssignmentMode::Exact; both names refer to the same
enum. Training still uses the selected approximate assignment algorithm.
Exact final assignment screens all centroids with blocked GEMM and refines
possible winners in float64; ties select the smallest centroid index. Returned
distances remain float32. Work is proportional to n * k * d; large coordinate
offsets or near ties can require more refinement.
Compare with FAISS¶
The comparison script
compares both methods with FAISS and requires faiss-cpu. Defaults are one
million 256-dimensional vectors, 4,000 centroids, and up to 25 iterations.
Load data with --input data.npy, or --input data.bin --d D for raw row-major
float32 data. Choose a thread count within the available hardware limit.
All three methods receive the same full dataset, iteration limit, cluster count, thread budget, and seed values; FAISS subsampling is disabled. This command runs four fits per method with rotating order. Different initializers mean identical seeds need not produce identical centroids.
Only train() is timed, including the library methods' default approximate
final assignment. The script reports actual iterations, timing medians/ranges,
exhaustive nearest-centroid objectives, and both library methods' returned-label
objectives and label mismatches. Equal-distance ties can produce mismatches
without changing the objective. --spherical normalizes the input copy and
compares sums of 1 - inner_product.
Separate worker processes isolate OpenMP runtimes and share a temporary float32
input file; allow about 4 * n * d bytes of temporary disk space. Input staging,
process startup, and exhaustive evaluation are outside training time. For Linux
CPU placement, set OMP_PLACES=threads OMP_PROC_BIND=close; on NUMA systems,
use numactl --cpunodebind=0 --membind=0 with a thread count that fits the node.