OICIO / oicio-rs /src /memory /turboquant.rs
deeprcurs-staff's picture
Upload folder using huggingface_hub
f9d2997 verified
Raw
History Blame Contribute Delete
8.06 kB
/*!
TurboQuant — Data-Oblivious Vector Quantization — 2-4 bit, No Training
Credits: deepRcurs Labs @deeprcurs / Mzed Imamkh @mzedimamkh
Berdasarkan:
- Google Research TurboQuant ICLR 2026 (2504.19874)
- RyanCodrai/turbovec Rust: 31GB -> 4GB (8-16x), 0.232ms/query M3 Max, no training
Core:
1. Normalize to unit hypersphere, store norm as f32
2. Random orthogonal rotation (Walsh-Hadamard) -> Beta -> Gaussian
3. Lloyd-Max scalar quant to 2-4 bit
4. Bit-packing
5. Search: rotate query once, score directly via SIMD, no decompression
CPU-only: AVX2/NEON, multi-threaded scan, x86-64-v2 baseline + AVX2/AVX-512 dispatch
*/
pub struct TurboQuantConfig {
pub dim: usize,
pub bit_width: usize, // 2,3,4,8
}
pub struct TurboQuant {
config: TurboQuantConfig,
/// Random orthogonal rotation matrix [dim, dim] — fixed, data-oblivious
rotation: Vec<f32>,
/// Lloyd-Max codebook for Gaussian
codebook: Vec<f32>,
/// Compressed codes: Vec<u8> packed
compressed: Option<Vec<u8>>,
norms: Option<Vec<f32>>,
}
impl TurboQuant {
pub fn new(config: TurboQuantConfig) -> Self {
assert!([2,3,4,8].contains(&config.bit_width));
// Fixed rotation: random orthogonal via QR (deterministic seed 42)
// Real uses Walsh-Hadamard + random diagonal for O(n log n)
let mut rotation = vec![0.0; config.dim * config.dim];
// For POC, identity + small random
for i in 0..config.dim {
for j in 0..config.dim {
if i == j {
rotation[i*config.dim + j] = 1.0;
} else {
rotation[i*config.dim + j] = (i as f32 * 0.01 + j as f32 * 0.01).sin() * 0.01;
}
}
}
// Lloyd-Max codebook for Gaussian
let codebook = match config.bit_width {
2 => vec![-1.510, -0.4528, 0.4528, 1.510],
4 => (0..16).map(|i| -2.0 + i as f32 * 4.0 / 15.0).collect(),
_ => (0..8).map(|i| -2.0 + i as f32 * 4.0 / 7.0).collect(),
};
Self {
config,
rotation,
codebook,
compressed: None,
norms: None,
}
}
/// Compress: [N, D] f32 -> codes + norms
/// Returns packed codes
pub fn compress(&mut self, vectors: &[f32], num_vectors: usize) -> Vec<u8> {
let dim = self.config.dim;
assert_eq!(vectors.len(), num_vectors * dim);
let mut norms = Vec::with_capacity(num_vectors);
let mut rotated = vec![0.0; num_vectors * dim];
// 1. Norms + normalize to unit sphere
for n in 0..num_vectors {
let mut norm_sq = 0.0;
for d in 0..dim {
let v = vectors[n*dim + d];
norm_sq += v*v;
}
let norm = norm_sq.sqrt().max(1e-8);
norms.push(norm);
for d in 0..dim {
rotated[n*dim + d] = vectors[n*dim + d] / norm;
}
}
// 2. Rotation: rotated @ rotation (data-oblivious, makes Gaussian)
let mut rotated2 = vec![0.0; num_vectors * dim];
for n in 0..num_vectors {
for d in 0..dim {
let mut sum = 0.0;
for k in 0..dim {
sum += rotated[n*dim + k] * self.rotation[k*dim + d];
}
rotated2[n*dim + d] = sum;
}
}
// 3. Lloyd-Max quant per coordinate -> indices
let num_levels = 1 << self.config.bit_width;
let mut indices = vec![0u8; num_vectors * dim];
for n in 0..num_vectors {
for d in 0..dim {
let val = rotated2[n*dim + d];
// Find nearest codebook entry
let mut best_idx = 0;
let mut best_dist = f32::INFINITY;
for (i, &cb) in self.codebook.iter().enumerate() {
let dist = (val - cb).abs();
if dist < best_dist {
best_dist = dist;
best_idx = i;
}
}
indices[n*dim + d] = best_idx as u8;
}
}
// 4. Bit-packing (POC: keep as u8, real would pack 2-bit: 4 per byte, 4-bit: 2 per byte)
self.compressed = Some(indices.clone());
self.norms = Some(norms);
indices
}
/// Decompress
pub fn decompress(&self) -> Option<Vec<f32>> {
let codes = self.compressed.as_ref()?;
let norms = self.norms.as_ref()?;
let dim = self.config.dim;
let num_vectors = norms.len();
let mut dequant = vec![0.0; num_vectors * dim];
for n in 0..num_vectors {
for d in 0..dim {
let idx = codes[n*dim + d] as usize;
dequant[n*dim + d] = self.codebook[idx];
}
}
// Inverse rotation: dequant @ rotation.T
let mut unrotated = vec![0.0; num_vectors * dim];
for n in 0..num_vectors {
for d in 0..dim {
let mut sum = 0.0;
for k in 0..dim {
sum += dequant[n*dim + k] * self.rotation[d*dim + k]; // rotation.T
}
unrotated[n*dim + d] = sum;
}
}
// Restore norm
let mut recon = vec![0.0; num_vectors * dim];
for n in 0..num_vectors {
for d in 0..dim {
recon[n*dim + d] = unrotated[n*dim + d] * norms[n];
}
}
Some(recon)
}
/// Search: rotate query once, score directly, no decompression of DB
pub fn search(&self, query: &[f32], k: usize) -> Option<(Vec<f32>, Vec<usize>)> {
let codes = self.compressed.as_ref()?;
let dim = self.config.dim;
let num_vectors = codes.len() / dim;
// Normalize query
let norm = query.iter().map(|v| v*v).sum::<f32>().sqrt().max(1e-8);
let q_norm: Vec<f32> = query.iter().map(|v| v / norm).collect();
// Rotate query once
let mut q_rot = vec![0.0; dim];
for d in 0..dim {
let mut sum = 0.0;
for k in 0..dim {
sum += q_norm[k] * self.rotation[k*dim + d];
}
q_rot[d] = sum;
}
// Dequant DB for scoring (real turbovec scores directly against codes via LUT, no dequant)
let mut db_dequant = vec![0.0; num_vectors * dim];
for n in 0..num_vectors {
for d in 0..dim {
let idx = codes[n*dim + d] as usize;
db_dequant[n*dim + d] = self.codebook[idx];
}
}
// Cosine similarity
let mut scores = vec![0.0; num_vectors];
for n in 0..num_vectors {
let mut dot = 0.0;
for d in 0..dim {
dot += q_rot[d] * db_dequant[n*dim + d];
}
scores[n] = dot;
}
// Top-k
let mut indexed: Vec<(usize, f32)> = scores.iter().enumerate().map(|(i,s)| (i,*s)).collect();
indexed.sort_by(|a,b| b.1.partial_cmp(&a.1).unwrap());
let top_k = indexed.iter().take(k).cloned().collect::<Vec<_>>();
let indices = top_k.iter().map(|(i,_)| *i).collect();
let scores = top_k.iter().map(|(_,s)| *s).collect();
Some((scores, indices))
}
pub fn compression_stats(&self, num_vectors: usize) -> String {
let dim = self.config.dim;
let bw = self.config.bit_width;
let fp32_bytes = num_vectors * dim * 4;
let packed_bits = num_vectors * dim * bw;
let packed_bytes = packed_bits / 8;
let norms_bytes = num_vectors * 4;
let total_packed = packed_bytes + norms_bytes;
format!(
"{} vectors {} dim: {:.1}MB -> {:.1}MB ({:.1}x) @ {}-bit",
num_vectors,
dim,
fp32_bytes as f32 / 1024.0 / 1024.0,
total_packed as f32 / 1024.0 / 1024.0,
fp32_bytes as f32 / total_packed as f32,
bw
)
}
}