Files
ruvnet--RuView/examples/ruvLLM/esp32/src/optimizations/product_quant.rs
T
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

337 lines
10 KiB
Rust

//! Product Quantization - 8-32x Memory Compression
//!
//! Adapted from ruvector-postgres for ESP32 constraints.
//! Splits vectors into subvectors and quantizes each independently.
use heapless::Vec as HVec;
/// Maximum number of subquantizers
pub const MAX_SUBQUANTIZERS: usize = 8;
/// Maximum codebook size per subquantizer
pub const MAX_CODEBOOK_SIZE: usize = 16; // 4-bit codes
/// Maximum subvector dimension
pub const MAX_SUBVEC_DIM: usize = 8;
/// Product Quantization configuration
#[derive(Debug, Clone, Copy)]
pub struct PQConfig {
/// Number of subquantizers (M)
pub num_subquantizers: usize,
/// Number of codes per subquantizer (K = 2^bits)
pub codebook_size: usize,
/// Dimension of each subvector
pub subvec_dim: usize,
/// Total vector dimension
pub dim: usize,
}
impl Default for PQConfig {
fn default() -> Self {
Self {
num_subquantizers: 4,
codebook_size: 16, // 4-bit codes
subvec_dim: 8,
dim: 32,
}
}
}
/// Product Quantized code for a vector
#[derive(Debug, Clone)]
pub struct PQCode<const M: usize> {
/// Code indices for each subquantizer (4-bit packed)
pub codes: HVec<u8, M>,
}
impl<const M: usize> PQCode<M> {
/// Create from code indices
pub fn from_codes(codes: &[u8]) -> crate::Result<Self> {
let mut code_vec = HVec::new();
for &c in codes {
code_vec.push(c).map_err(|_| crate::Error::BufferOverflow)?;
}
Ok(Self { codes: code_vec })
}
/// Get code for subquantizer i
#[inline]
pub fn get_code(&self, i: usize) -> u8 {
self.codes.get(i).copied().unwrap_or(0)
}
/// Memory size in bytes
pub fn memory_size(&self) -> usize {
self.codes.len()
}
}
/// Product Quantizer with codebooks
pub struct ProductQuantizer<const M: usize, const K: usize, const D: usize> {
/// Codebooks: [M][K][D] flattened to [M * K * D]
/// Each subquantizer has K centroids of dimension D
codebooks: HVec<i8, { 8 * 16 * 8 }>, // Max 1024 bytes
/// Configuration
config: PQConfig,
}
impl<const M: usize, const K: usize, const D: usize> ProductQuantizer<M, K, D> {
/// Create with random codebooks (for testing)
pub fn random(config: PQConfig, seed: u32) -> crate::Result<Self> {
let total_size = config.num_subquantizers * config.codebook_size * config.subvec_dim;
let mut codebooks = HVec::new();
let mut rng_state = seed;
for _ in 0..total_size {
rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
let val = (((rng_state >> 16) & 0xFF) as i16 - 128) as i8;
codebooks.push(val).map_err(|_| crate::Error::BufferOverflow)?;
}
Ok(Self { codebooks, config })
}
/// Create from pre-trained codebooks
pub fn from_codebooks(config: PQConfig, codebooks: &[i8]) -> crate::Result<Self> {
let expected = config.num_subquantizers * config.codebook_size * config.subvec_dim;
if codebooks.len() != expected {
return Err(crate::Error::InvalidModel("Codebook size mismatch"));
}
let mut cb_vec = HVec::new();
for &v in codebooks {
cb_vec.push(v).map_err(|_| crate::Error::BufferOverflow)?;
}
Ok(Self { codebooks: cb_vec, config })
}
/// Get centroid for subquantizer m, code k
#[inline]
fn get_centroid(&self, m: usize, k: usize) -> &[i8] {
let d = self.config.subvec_dim;
let kk = self.config.codebook_size;
let start = m * kk * d + k * d;
&self.codebooks[start..start + d]
}
/// Encode a vector to PQ codes
pub fn encode(&self, vector: &[i8]) -> crate::Result<PQCode<M>> {
if vector.len() != self.config.dim {
return Err(crate::Error::InvalidModel("Vector dimension mismatch"));
}
let mut codes = HVec::new();
let d = self.config.subvec_dim;
for m in 0..self.config.num_subquantizers {
let subvec = &vector[m * d..(m + 1) * d];
// Find nearest centroid
let mut best_code = 0u8;
let mut best_dist = i32::MAX;
for k in 0..self.config.codebook_size {
let centroid = self.get_centroid(m, k);
let dist = Self::l2_squared(subvec, centroid);
if dist < best_dist {
best_dist = dist;
best_code = k as u8;
}
}
codes.push(best_code).map_err(|_| crate::Error::BufferOverflow)?;
}
Ok(PQCode { codes })
}
/// Decode PQ codes back to approximate vector
pub fn decode(&self, code: &PQCode<M>, output: &mut [i8]) -> crate::Result<()> {
if output.len() != self.config.dim {
return Err(crate::Error::InvalidModel("Output dimension mismatch"));
}
let d = self.config.subvec_dim;
for m in 0..self.config.num_subquantizers {
let k = code.get_code(m) as usize;
let centroid = self.get_centroid(m, k);
output[m * d..(m + 1) * d].copy_from_slice(centroid);
}
Ok(())
}
/// Compute asymmetric distance: exact query vs PQ-encoded database vector
pub fn asymmetric_distance(&self, query: &[i8], code: &PQCode<M>) -> i32 {
let d = self.config.subvec_dim;
let mut total_dist: i32 = 0;
for m in 0..self.config.num_subquantizers {
let query_sub = &query[m * d..(m + 1) * d];
let k = code.get_code(m) as usize;
let centroid = self.get_centroid(m, k);
total_dist += Self::l2_squared(query_sub, centroid);
}
total_dist
}
/// Compute distance using pre-computed distance table (faster for batch queries)
pub fn distance_with_table(&self, table: &PQDistanceTable<M, K>, code: &PQCode<M>) -> i32 {
let mut total: i32 = 0;
for m in 0..self.config.num_subquantizers {
let k = code.get_code(m) as usize;
total += table.get(m, k);
}
total
}
/// Build distance table for a query (precompute all query-centroid distances)
pub fn build_distance_table(&self, query: &[i8]) -> PQDistanceTable<M, K> {
let mut table = PQDistanceTable::new();
let d = self.config.subvec_dim;
for m in 0..self.config.num_subquantizers {
let query_sub = &query[m * d..(m + 1) * d];
for k in 0..self.config.codebook_size {
let centroid = self.get_centroid(m, k);
let dist = Self::l2_squared(query_sub, centroid);
table.set(m, k, dist);
}
}
table
}
/// L2 squared distance between two INT8 vectors
#[inline]
fn l2_squared(a: &[i8], b: &[i8]) -> i32 {
let mut sum: i32 = 0;
for (&x, &y) in a.iter().zip(b.iter()) {
let diff = x as i32 - y as i32;
sum += diff * diff;
}
sum
}
/// Memory usage of codebooks
pub fn memory_size(&self) -> usize {
self.codebooks.len()
}
/// Compression ratio vs INT8
pub fn compression_ratio(&self) -> f32 {
let original = self.config.dim as f32; // 1 byte per dim
let compressed = self.config.num_subquantizers as f32; // 1 byte per code
original / compressed
}
}
/// Pre-computed distance table for fast PQ distance computation
pub struct PQDistanceTable<const M: usize, const K: usize> {
/// Distances: [M][K] flattened
distances: [i32; 128], // Max 8 subquantizers * 16 codes
}
impl<const M: usize, const K: usize> PQDistanceTable<M, K> {
/// Create empty table
pub fn new() -> Self {
Self { distances: [0; 128] }
}
/// Get distance for subquantizer m, code k
#[inline]
pub fn get(&self, m: usize, k: usize) -> i32 {
self.distances[m * K + k]
}
/// Set distance for subquantizer m, code k
#[inline]
pub fn set(&mut self, m: usize, k: usize, dist: i32) {
self.distances[m * K + k] = dist;
}
}
impl<const M: usize, const K: usize> Default for PQDistanceTable<M, K> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pq_config() {
let config = PQConfig::default();
assert_eq!(config.num_subquantizers, 4);
assert_eq!(config.codebook_size, 16);
assert_eq!(config.subvec_dim, 8);
assert_eq!(config.dim, 32);
}
#[test]
fn test_pq_encode_decode() {
let config = PQConfig {
num_subquantizers: 4,
codebook_size: 16,
subvec_dim: 8,
dim: 32,
};
let pq = ProductQuantizer::<4, 16, 8>::random(config, 42).unwrap();
// Create a test vector
let mut vector = [0i8; 32];
for i in 0..32 {
vector[i] = (i as i8).wrapping_mul(3);
}
// Encode
let code = pq.encode(&vector).unwrap();
assert_eq!(code.codes.len(), 4);
// Decode
let mut decoded = [0i8; 32];
pq.decode(&code, &mut decoded).unwrap();
// Decoded should be approximate (using centroids)
// Just verify it runs without error
}
#[test]
fn test_pq_compression() {
let config = PQConfig::default();
let pq = ProductQuantizer::<4, 16, 8>::random(config, 42).unwrap();
// 32 bytes original -> 4 bytes codes = 8x compression
assert_eq!(pq.compression_ratio(), 8.0);
}
#[test]
fn test_distance_table() {
let config = PQConfig::default();
let pq = ProductQuantizer::<4, 16, 8>::random(config, 42).unwrap();
let mut query = [0i8; 32];
for i in 0..32 {
query[i] = i as i8;
}
let table = pq.build_distance_table(&query);
// Encode a vector and compute distance both ways
let mut vector = [10i8; 32];
let code = pq.encode(&vector).unwrap();
let dist1 = pq.asymmetric_distance(&query, &code);
let dist2 = pq.distance_with_table(&table, &code);
// Should be equal
assert_eq!(dist1, dist2);
}
}