33
44//! Max-Lloyd centroid computation for TurboQuant scalar quantizers.
55//!
6- //! Pre-computes optimal scalar quantizer centroids for the marginal distribution of coordinates
7- //! after random rotation of a unit-norm vector. In high dimensions, each coordinate of a randomly
8- //! rotated unit vector follows a distribution proportional to `(1 - x^2)^((d-3)/2)` on `[-1, 1]`,
9- //! which converges to `N(0, 1/d)`. The Max-Lloyd algorithm finds optimal quantization centroids
10- //! that minimize MSE for this distribution.
6+ //! Pre-computes and caches optimal scalar quantizer centroids for the marginal distribution of
7+ //! coordinates after a random orthogonal transform of a unit-norm vector.
8+ //!
9+ //! In high dimensions, each coordinate of a randomly transformed unit vector follows a
10+ //! distribution proportional to `(1 - x^2)^((d-3)/2)` on `[-1, 1]`, which converges to
11+ //! `N(0, 1/d)`.
12+ //!
13+ //! The Max-Lloyd algorithm finds optimal quantization centroids that minimize MSE for this
14+ //! distribution.
15+ //!
16+ //! Centroids are not stored in TurboQuant arrays. They are deterministically derived from
17+ //! `(padded_dim, bit_width)` and cached process-locally.
18+ //!
19+ //! The centroid model follows the random orthogonal transform marginal used by the TurboQuant
20+ //! paper. This encoder applies a SORF-style structured transform instead of a dense random Gaussian
21+ //! or orthogonal matrix, so paper-level error bounds should not be treated as verified for this
22+ //! implementation without separate empirical validation.
1123
1224use std:: sync:: LazyLock ;
1325
@@ -34,9 +46,9 @@ static CENTROID_CACHE: LazyLock<DashMap<(u32, u8), Buffer<f32>>> = LazyLock::new
3446/// Get or compute cached centroids for the given dimension and bit width.
3547///
3648/// Returns `2^bit_width` centroids sorted in ascending order, representing optimal scalar
37- /// quantization levels for the coordinate distribution after random rotation in
49+ /// quantization levels for the coordinate distribution after a random orthogonal transform in
3850/// `dimension`-dimensional space.
39- pub fn compute_or_get_centroids ( dimension : u32 , bit_width : u8 ) -> VortexResult < Buffer < f32 > > {
51+ pub ( crate ) fn compute_or_get_centroids ( dimension : u32 , bit_width : u8 ) -> VortexResult < Buffer < f32 > > {
4052 vortex_ensure ! (
4153 ( 1 ..=MAX_BIT_WIDTH ) . contains( & bit_width) ,
4254 "TurboQuant bit_width must be 1-{}, got {bit_width}" ,
@@ -86,22 +98,35 @@ impl HalfIntExponent {
8698
8799/// Compute optimal centroids via the Max-Lloyd (Lloyd-Max) algorithm.
88100///
89- /// Operates on the marginal distribution of a single coordinate of a randomly rotated unit vector
90- /// in d dimensions.
101+ /// Operates on the marginal distribution of a single coordinate of a randomly transformed unit
102+ /// vector in d dimensions.
91103///
92104/// The probability distribution function is:
93105/// `f(x) = C_d * (1 - x^2)^((d-3)/2)` on `[-1, 1]`
94106/// where `C_d` is the normalizing constant.
107+ ///
108+ /// Centroids are seeded uniformly on `[±sqrt(bit_width) * sigma]` (where `sigma` is the standard
109+ /// deviation of the normal distribution that hyphershere dimension values take, and specifically
110+ /// `sigma = 1/sqrt( dimension)`) rather than across the full `[-1, 1]`, which strands most of the
111+ /// centroids in the near-zero-mass tails.
112+ ///
113+ /// Note that the `sqrt(bit_width)` is mostly empirically derived, we do not have a theoretical
114+ /// basis for choosing this other than the fact that it seems to produce good results.
95115fn max_lloyd_centroids ( dimension : u32 , bit_width : u8 ) -> Buffer < f32 > {
96116 debug_assert ! ( ( 1 ..=MAX_BIT_WIDTH ) . contains( & bit_width) ) ;
97117 let num_centroids = 1usize << bit_width;
98118
99119 // For the marginal distribution on [-1, 1], we use the exponent (d-3)/2.
100120 let exponent = HalfIntExponent :: from_numerator ( dimension as i32 - 3 ) ;
101121
102- // Initialize centroids uniformly on [-1, 1].
122+ // The coordinate marginal concentrates around 0 with this standard deviation.
123+ let sigma = 1.0 / f64:: from ( dimension) . sqrt ( ) ;
124+ let init_half = ( f64:: from ( bit_width) . sqrt ( ) * sigma) . min ( 1.0 ) ;
125+
126+ // Initialize centroids uniformly on [-init_half, init_half], where the mass lives, so no cell
127+ // starts in a zero-mass region and freezes.
103128 let mut centroids: Vec < f64 > = ( 0 ..num_centroids)
104- . map ( |idx| -1.0 + ( 2.0 * ( idx as f64 ) + 1.0 ) / ( num_centroids as f64 ) )
129+ . map ( |idx| -init_half + ( 2.0 * ( idx as f64 ) + 1.0 ) * init_half / ( num_centroids as f64 ) )
105130 . collect ( ) ;
106131
107132 let mut boundaries: Vec < f64 > = vec ! [ 0.0 ; num_centroids + 1 ] ;
@@ -193,7 +218,7 @@ fn pdf_unnormalized(x_val: f64, exponent: HalfIntExponent) -> f64 {
193218/// For `k` centroids, returns `k-1` boundaries. A value below `boundaries[0]` maps to centroid 0, a
194219/// value in `[boundaries[i-1], boundaries[i])` maps to centroid `i`, and a
195220/// value `>= boundaries[k-2]` maps to centroid `k-1`.
196- pub fn compute_centroid_boundaries ( centroids : & [ f32 ] ) -> Vec < f32 > {
221+ pub ( crate ) fn compute_centroid_boundaries ( centroids : & [ f32 ] ) -> Vec < f32 > {
197222 centroids. windows ( 2 ) . map ( |w| ( w[ 0 ] + w[ 1 ] ) * 0.5 ) . collect ( )
198223}
199224
@@ -203,7 +228,7 @@ pub fn compute_centroid_boundaries(centroids: &[f32]) -> Vec<f32> {
203228/// centroids. Uses binary search on the midpoints, avoiding distance comparisons
204229/// in the inner loop.
205230#[ inline]
206- pub fn find_nearest_centroid ( value : f32 , boundaries : & [ f32 ] ) -> u8 {
231+ pub ( crate ) fn find_nearest_centroid ( value : f32 , boundaries : & [ f32 ] ) -> u8 {
207232 debug_assert ! (
208233 boundaries. windows( 2 ) . all( |w| w[ 0 ] <= w[ 1 ] ) ,
209234 "boundaries must be sorted"
0 commit comments