Skip to content

Commit f8c6551

Browse files
committed
use 'transform' instead of 'rotate'
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent 577d801 commit f8c6551

8 files changed

Lines changed: 73 additions & 71 deletions

File tree

vortex-turboquant/src/centroids.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,21 @@
44
//! Max-Lloyd centroid computation for TurboQuant scalar quantizers.
55
//!
66
//! Pre-computes and caches optimal scalar quantizer centroids for the marginal distribution of
7-
//! coordinates after random rotation of a unit-norm vector.
7+
//! coordinates after a random orthogonal transform of a unit-norm vector.
88
//!
9-
//! In high dimensions, each coordinate of a randomly rotated unit vector follows a distribution
10-
//! proportional to `(1 - x^2)^((d-3)/2)` on `[-1, 1]`, which converges to `N(0, 1/d)`.
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)`.
1112
//!
1213
//! The Max-Lloyd algorithm finds optimal quantization centroids that minimize MSE for this
1314
//! distribution.
1415
//!
1516
//! Centroids are not stored in TurboQuant arrays. They are deterministically derived from
1617
//! `(padded_dim, bit_width)` and cached process-locally.
1718
//!
18-
//! The centroid model follows the random-rotation marginal used by the TurboQuant paper. This
19-
//! encoder applies a SORF-style structured rotation instead of a dense random Gaussian or
20-
//! orthogonal matrix, so paper-level error bounds should not be treated as verified for this
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
2122
//! implementation without separate empirical validation.
2223
2324
use std::sync::LazyLock;
@@ -47,7 +48,7 @@ static CENTROID_CACHE: LazyLock<DashMap<(u32, u8), Buffer<f32>>> = LazyLock::new
4748
/// Get or compute cached centroids for the given dimension and bit width.
4849
///
4950
/// Returns `2^bit_width` centroids sorted in ascending order, representing optimal scalar
50-
/// quantization levels for the coordinate distribution after random rotation in
51+
/// quantization levels for the coordinate distribution after a random orthogonal transform in
5152
/// `dimension`-dimensional space.
5253
pub(crate) fn compute_or_get_centroids(dimension: u32, bit_width: u8) -> VortexResult<Buffer<f32>> {
5354
vortex_ensure!(
@@ -99,8 +100,8 @@ impl HalfIntExponent {
99100

100101
/// Compute optimal centroids via the Max-Lloyd (Lloyd-Max) algorithm.
101102
///
102-
/// Operates on the marginal distribution of a single coordinate of a randomly rotated unit vector
103-
/// in d dimensions.
103+
/// Operates on the marginal distribution of a single coordinate of a randomly transformed unit
104+
/// vector in d dimensions.
104105
///
105106
/// The probability distribution function is:
106107
/// `f(x) = C_d * (1 - x^2)^((d-3)/2)` on `[-1, 1]`

vortex-turboquant/src/config.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ impl TurboQuantConfig {
4949
self.bit_width
5050
}
5151

52-
/// Seed used to derive the deterministic SORF rotation.
52+
/// Seed used to derive the deterministic SORF transform.
5353
pub fn seed(&self) -> u64 {
5454
self.seed
5555
}
5656

57-
/// Number of sign-diagonal plus Walsh-Hadamard rounds in the SORF rotation.
57+
/// Number of sign-diagonal plus Walsh-Hadamard rounds in the SORF transform.
5858
pub fn num_rounds(&self) -> u8 {
5959
self.num_rounds
6060
}

vortex-turboquant/src/lib.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@
66
//! Implements a Stage 1 TurboQuant encoding ([arXiv:2504.19874], [RFC 0033]) for lossy compression
77
//! of high-dimensional vector data. The extension operates on
88
//! [`Vector`](vortex_tensor::vector::Vector) extension arrays, packing their `FixedSizeList`
9-
//! storage into quantized codes after a structured orthogonal surrogate rotation.
9+
//! storage into quantized codes after a structured orthogonal surrogate transform.
1010
//!
1111
//! [arXiv:2504.19874]: https://arxiv.org/abs/2504.19874
1212
//! [RFC 0033]: https://vortex-data.github.io/rfcs/rfc/0033.html
1313
//!
1414
//! # Overview
1515
//!
1616
//! TurboQuant minimizes mean-squared reconstruction error (1-8 bits per coordinate)
17-
//! using MSE-optimal scalar quantization on coordinates of a rotated unit vector.
17+
//! using MSE-optimal scalar quantization on coordinates of a transformed unit vector.
1818
//!
1919
//! The [`turboquant_pack()`] path first computes and stores the original L2 norm for each vector
20-
//! row, then normalizes each valid nonzero row internally before SORF rotation and scalar
20+
//! row, then normalizes each valid nonzero row internally before SORF transform and scalar
2121
//! quantization. The [`turboquant_unpack()`] path dequantizes through deterministic centroids,
22-
//! applies the inverse SORF rotation, truncates back to the original dimension, and re-applies the
22+
//! applies the inverse SORF transform, truncates back to the original dimension, and re-applies the
2323
//! stored norm.
2424
//!
2525
//! [`turboquant_pack()`]: crate::turboquant_pack
@@ -37,7 +37,7 @@
3737
//! ```
3838
//!
3939
//! Stored norms are authoritative for future TurboQuant-aware scalar functions. Decoded quantized
40-
//! directions are not guaranteed to have unit norm after scalar quantization and inverse rotation.
40+
//! directions are not guaranteed to have unit norm after scalar quantization and inverse transform.
4141
//!
4242
//! # Source map
4343
//!
@@ -47,10 +47,10 @@
4747
//! validity for null vectors.
4848
//! - `vector/normalize.rs`: TurboQuant-local normalization and how it differs from the tensor
4949
//! crate's null-row zeroing helper.
50-
//! - `vector/quantize.rs`: SORF rotation, centroid lookup, and why invalid rows are skipped rather
50+
//! - `vector/quantize.rs`: SORF transform, centroid lookup, and why invalid rows are skipped rather
5151
//! than quantized.
5252
//! - `centroids.rs`: deterministic Max-Lloyd centroid computation and process-local caching.
53-
//! - `sorf/`: the Walsh-Hadamard-based structured rotation and the stable SplitMix64 sign stream.
53+
//! - `sorf/`: the Walsh-Hadamard-based structured transform and the stable SplitMix64 sign stream.
5454
//!
5555
//! The current encoding is intentionally MSE-only. It does not yet implement the paper's QJL
5656
//! residual correction for unbiased inner-product estimation, and it still uses internal

vortex-turboquant/src/sorf/transform.rs

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
//! This makes SORF sign generation stable as an extension format contract even if external RNG
2929
//! implementations change.
3030
//!
31-
//! This transform is the crate's practical structured-rotation choice for TurboQuant. It is not
31+
//! This transform is the crate's practical structured transform choice for TurboQuant. It is not
3232
//! the dense random Gaussian or orthogonal matrix used by some theoretical analyses, so theoretical
3333
//! bounds from those models need separate validation before being presented as implementation
3434
//! guarantees.
@@ -118,7 +118,7 @@ impl SorfMatrix {
118118

119119
/// Returns the padded dimension (next power of 2 >= dim).
120120
///
121-
/// All `rotate`/`inverse_rotate` buffers must be this length.
121+
/// All `transform`/`inverse_transform` buffers must be this length.
122122
pub(crate) fn padded_dim(&self) -> usize {
123123
self.padded_dim
124124
}
@@ -127,7 +127,7 @@ impl SorfMatrix {
127127
///
128128
/// Both `input` and `output` must have length [`padded_dim()`](Self::padded_dim). The caller is
129129
/// responsible for zero-padding input beyond `dim` positions.
130-
pub(crate) fn rotate(&self, input: &[f32], output: &mut [f32]) {
130+
pub(crate) fn transform(&self, input: &[f32], output: &mut [f32]) {
131131
debug_assert_eq!(input.len(), self.padded_dim);
132132
debug_assert_eq!(output.len(), self.padded_dim);
133133

@@ -138,7 +138,7 @@ impl SorfMatrix {
138138
/// Apply the inverse orthogonal transform: `output = R⁻¹(input)`.
139139
///
140140
/// Both `input` and `output` must have length `padded_dim()`.
141-
pub(crate) fn inverse_rotate(&self, input: &[f32], output: &mut [f32]) {
141+
pub(crate) fn inverse_transform(&self, input: &[f32], output: &mut [f32]) {
142142
debug_assert_eq!(input.len(), self.padded_dim);
143143
debug_assert_eq!(output.len(), self.padded_dim);
144144

@@ -355,9 +355,9 @@ mod tests {
355355
let padded_dim = dim_to_usize(64u32);
356356
let num_rounds = rounds_to_usize(3u8);
357357
let seed = 42u64;
358-
let r1 = SorfMatrix::try_new(padded_dim, num_rounds, seed)?;
359-
let r2 = SorfMatrix::try_new(padded_dim, num_rounds, seed)?;
360-
let pd = r1.padded_dim();
358+
let transform1 = SorfMatrix::try_new(padded_dim, num_rounds, seed)?;
359+
let transform2 = SorfMatrix::try_new(padded_dim, num_rounds, seed)?;
360+
let pd = transform1.padded_dim();
361361

362362
let mut input = vec![0.0f32; pd];
363363
for i in 0..padded_dim {
@@ -366,8 +366,8 @@ mod tests {
366366
let mut out1 = vec![0.0f32; pd];
367367
let mut out2 = vec![0.0f32; pd];
368368

369-
r1.rotate(&input, &mut out1);
370-
r2.rotate(&input, &mut out2);
369+
transform1.transform(&input, &mut out1);
370+
transform2.transform(&input, &mut out2);
371371

372372
assert_eq!(out1, out2);
373373
Ok(())
@@ -378,8 +378,8 @@ mod tests {
378378
let padded_dim = dim_to_usize(64u32);
379379
let num_rounds = rounds_to_usize(2u8);
380380
let seed = 42u64;
381-
let rot = SorfMatrix::try_new(padded_dim, num_rounds, seed)?;
382-
let actual = rot.export_inverse_signs_u8();
381+
let transform = SorfMatrix::try_new(padded_dim, num_rounds, seed)?;
382+
let actual = transform.export_inverse_signs_u8();
383383
let mut rng = SplitMix64::new(seed);
384384
let round0_word = rng.next_u64();
385385
let round1_word = rng.next_u64();
@@ -446,18 +446,18 @@ mod tests {
446446
fn roundtrip_exact(#[case] dim: u32, #[case] num_rounds: u8) -> VortexResult<()> {
447447
let dim = dim_to_usize(dim);
448448
let num_rounds = rounds_to_usize(num_rounds);
449-
let rot = SorfMatrix::try_new(dim.next_power_of_two(), num_rounds, 42u64)?;
450-
let padded_dim = rot.padded_dim();
449+
let transform = SorfMatrix::try_new(dim.next_power_of_two(), num_rounds, 42u64)?;
450+
let padded_dim = transform.padded_dim();
451451

452452
let mut input = vec![0.0f32; padded_dim];
453453
for i in 0..dim {
454454
input[i] = (i as f32 + 1.0) * 0.01;
455455
}
456-
let mut rotated = vec![0.0f32; padded_dim];
456+
let mut transformed = vec![0.0f32; padded_dim];
457457
let mut recovered = vec![0.0f32; padded_dim];
458458

459-
rot.rotate(&input, &mut rotated);
460-
rot.inverse_rotate(&rotated, &mut recovered);
459+
transform.transform(&input, &mut transformed);
460+
transform.inverse_transform(&transformed, &mut recovered);
461461

462462
let max_err: f32 = input
463463
.iter()
@@ -484,25 +484,25 @@ mod tests {
484484
fn preserves_norm(#[case] dim: u32, #[case] num_rounds: u8) -> VortexResult<()> {
485485
let dim = dim_to_usize(dim);
486486
let num_rounds = rounds_to_usize(num_rounds);
487-
let rot = SorfMatrix::try_new(dim.next_power_of_two(), num_rounds, 7u64)?;
488-
let padded_dim = rot.padded_dim();
487+
let transform = SorfMatrix::try_new(dim.next_power_of_two(), num_rounds, 7u64)?;
488+
let padded_dim = transform.padded_dim();
489489

490490
let mut input = vec![0.0f32; padded_dim];
491491
for i in 0..dim {
492492
input[i] = (i as f32) * 0.01;
493493
}
494494
let input_norm: f32 = input.iter().map(|x| x * x).sum::<f32>().sqrt();
495495

496-
let mut rotated = vec![0.0f32; padded_dim];
497-
rot.rotate(&input, &mut rotated);
498-
let rotated_norm: f32 = rotated.iter().map(|x| x * x).sum::<f32>().sqrt();
496+
let mut transformed = vec![0.0f32; padded_dim];
497+
transform.transform(&input, &mut transformed);
498+
let transformed_norm: f32 = transformed.iter().map(|x| x * x).sum::<f32>().sqrt();
499499

500500
assert!(
501-
(input_norm - rotated_norm).abs() / input_norm < 1e-5,
501+
(input_norm - transformed_norm).abs() / input_norm < 1e-5,
502502
"norm not preserved for dim={dim}: {} vs {} (rel err: {:.2e})",
503503
input_norm,
504-
rotated_norm,
505-
(input_norm - rotated_norm).abs() / input_norm
504+
transformed_norm,
505+
(input_norm - transformed_norm).abs() / input_norm
506506
);
507507
Ok(())
508508
}
@@ -517,11 +517,11 @@ mod tests {
517517
fn sign_export_import_roundtrip(#[case] dim: u32, #[case] num_rounds: u8) -> VortexResult<()> {
518518
let dim = dim_to_usize(dim);
519519
let num_rounds = rounds_to_usize(num_rounds);
520-
let rot = SorfMatrix::try_new(dim.next_power_of_two(), num_rounds, 42u64)?;
521-
let padded_dim = rot.padded_dim();
520+
let transform = SorfMatrix::try_new(dim.next_power_of_two(), num_rounds, 42u64)?;
521+
let padded_dim = transform.padded_dim();
522522

523-
let signs_u8 = rot.export_inverse_signs_u8();
524-
let rot2 = SorfMatrix::from_u8_slice(&signs_u8, dim, num_rounds)?;
523+
let signs_u8 = transform.export_inverse_signs_u8();
524+
let transform2 = SorfMatrix::from_u8_slice(&signs_u8, dim, num_rounds)?;
525525

526526
let mut input = vec![0.0f32; padded_dim];
527527
for i in 0..dim {
@@ -530,13 +530,13 @@ mod tests {
530530

531531
let mut out1 = vec![0.0f32; padded_dim];
532532
let mut out2 = vec![0.0f32; padded_dim];
533-
rot.rotate(&input, &mut out1);
534-
rot2.rotate(&input, &mut out2);
533+
transform.transform(&input, &mut out1);
534+
transform2.transform(&input, &mut out2);
535535
assert_eq!(out1, out2, "Forward transform mismatch after export/import");
536536

537-
rot.inverse_rotate(&out1, &mut out2);
537+
transform.inverse_transform(&out1, &mut out2);
538538
let mut out3 = vec![0.0f32; padded_dim];
539-
rot2.inverse_rotate(&out1, &mut out3);
539+
transform2.inverse_transform(&out1, &mut out3);
540540
assert_eq!(out2, out3, "Inverse transform mismatch after export/import");
541541

542542
Ok(())

vortex-turboquant/src/vector/pack.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use crate::vtable::TurboQuantMetadata;
3434

3535
/// Lossily pack a `Vector` extension array into a `TurboQuant` extension array.
3636
///
37-
/// Valid rows are normalized internally before SORF rotation and scalar quantization. The original
37+
/// Valid rows are normalized internally before SORF transform and scalar quantization. The original
3838
/// row norms are stored explicitly, and original vector nulls are preserved on the storage struct
3939
/// and both row-aligned child arrays.
4040
pub fn turboquant_pack(

vortex-turboquant/src/vector/quantize.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
//! Core TurboQuant quantization helpers.
55
//!
6-
//! Quantization consumes the TurboQuant-local normalized `Vector` child. Valid rows are rotated and
7-
//! mapped to scalar centroid indices. Invalid rows remain in the full-length output but are
6+
//! Quantization consumes the TurboQuant-local normalized `Vector` child. Valid rows are transformed
7+
//! and mapped to scalar centroid indices. Invalid rows remain in the full-length output but are
88
//! skipped: their physical code bytes are placeholders guarded by the `codes` row validity.
99
//!
10-
//! This matters because TurboQuant's scalar codebook is optimized for coordinates of rotated
10+
//! This matters because TurboQuant's scalar codebook is optimized for coordinates of transformed
1111
//! unit-norm vectors. The codebook does not generally contain an exact zero centroid, and a
1212
//! physical code byte of `0` means "centroid 0", not "zero coordinate". Null vectors therefore
1313
//! should not be converted to zero vectors and fed through the quantizer.
@@ -44,13 +44,13 @@ pub(crate) fn empty_quantization(padded_dim: usize) -> QuantizationResult {
4444
}
4545
}
4646

47-
/// Core quantization: rotate and quantize already-normalized rows.
47+
/// Core quantization: transform and quantize already-normalized rows.
4848
///
4949
/// # Safety
5050
///
5151
/// The input `fsl` must contain unit-norm vectors (already L2-normalized) for every valid row.
52-
/// Invalid rows are left row-aligned in the output but are not rotated or quantized. The rotation
53-
/// and centroid lookup happen in f32.
52+
/// Invalid rows are left row-aligned in the output but are not transformed or quantized. The
53+
/// transform and centroid lookup happen in f32.
5454
pub(crate) unsafe fn turboquant_quantize_core(
5555
fsl: &FixedSizeListArray,
5656
config: &TurboQuantConfig,
@@ -60,8 +60,9 @@ pub(crate) unsafe fn turboquant_quantize_core(
6060
let num_vectors = fsl.len();
6161
let padded_dim = tq_padded_dim(dimension)?;
6262

63-
let rotation = SorfMatrix::try_new(padded_dim, config.num_rounds() as usize, config.seed())?;
64-
debug_assert_eq!(rotation.padded_dim(), padded_dim);
63+
let sorf_transform =
64+
SorfMatrix::try_new(padded_dim, config.num_rounds() as usize, config.seed())?;
65+
debug_assert_eq!(sorf_transform.padded_dim(), padded_dim);
6566
let padded_dim_u32 = u32::try_from(padded_dim)
6667
.map_err(|_| vortex_err!("TurboQuant padded dimension does not fit u32"))?;
6768

@@ -78,7 +79,7 @@ pub(crate) unsafe fn turboquant_quantize_core(
7879
.ok_or_else(|| vortex_err!("TurboQuant codes length overflow"))?;
7980
let mut all_indices = BufferMut::<u8>::with_capacity(codes_len);
8081
let mut padded = vec![0.0f32; padded_dim];
81-
let mut rotated = vec![0.0f32; padded_dim];
82+
let mut transformed = vec![0.0f32; padded_dim];
8283

8384
let f32_slice = f32_elements.as_slice();
8485
let dimension = dimension as usize;
@@ -100,11 +101,11 @@ pub(crate) unsafe fn turboquant_quantize_core(
100101
padded[..dimension].copy_from_slice(x);
101102
padded[dimension..].fill(0.0);
102103

103-
rotation.rotate(&padded, &mut rotated);
104+
sorf_transform.transform(&padded, &mut transformed);
104105

105106
// SAFETY: `all_indices` was allocated with capacity `codes_len`, and the loop appends
106107
// exactly `padded_dim` codes for each of `num_vectors` iterations.
107-
for &value in rotated.iter() {
108+
for &value in transformed.iter() {
108109
unsafe { all_indices.push_unchecked(find_nearest_centroid(value, &boundaries)) };
109110
}
110111
}

0 commit comments

Comments
 (0)