hermes_core/structures/vector/quantization/
rabitq.rs1use rand::prelude::*;
14use serde::{Deserialize, Serialize};
15
16use super::super::ivf::cluster::QuantizedCode;
17use super::Quantizer;
18
19#[cfg(target_arch = "aarch64")]
20#[allow(unused_imports)]
21use std::arch::aarch64::*;
22
23#[cfg(target_arch = "x86_64")]
24#[allow(unused_imports)]
25use std::arch::x86_64::*;
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct RaBitQConfig {
30 pub dim: usize,
32 pub query_bits: u8,
34 pub seed: u64,
36 #[serde(default)]
48 pub ex_bits: u8,
49}
50
51impl RaBitQConfig {
52 pub fn new(dim: usize) -> Self {
53 Self {
54 dim,
55 query_bits: 4,
56 seed: 42,
57 ex_bits: 0,
58 }
59 }
60
61 pub fn with_seed(mut self, seed: u64) -> Self {
62 self.seed = seed;
63 self
64 }
65
66 pub fn with_bits(mut self, total_bits: u8) -> Self {
68 self.ex_bits = total_bits.clamp(1, 8) - 1;
69 self
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct QuantizedVector {
76 pub bits: Vec<u8>,
78 pub dist_to_centroid: f32,
80 pub self_dot: f32,
82 pub popcount: u32,
84 #[serde(default, skip_serializing_if = "Vec::is_empty")]
88 pub ex_code: Vec<u8>,
89 #[serde(default)]
91 pub ex_scale: f32,
92 #[serde(default)]
95 pub ex_norm: f32,
96}
97
98impl QuantizedCode for QuantizedVector {
99 fn size_bytes(&self) -> usize {
100 self.bits.len() + 4 + 4 + 4 + self.ex_code.len() + 8
102 }
103}
104
105#[derive(Debug, Clone)]
107pub struct QuantizedQuery {
108 pub quantized: Vec<u8>,
110 pub dist_to_centroid: f32,
112 pub lower: f32,
114 pub width: f32,
116 pub sum: u32,
118 pub luts: Vec<[u16; 16]>,
120 pub transformed: Vec<f32>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct RaBitQCodebook {
130 pub config: RaBitQConfig,
132 pub random_signs: Vec<i8>,
134 pub random_perm: Vec<u32>,
136 pub version: u64,
138}
139
140impl RaBitQCodebook {
141 pub fn new(config: RaBitQConfig) -> Self {
143 let dim = config.dim;
144 let mut rng = rand::rngs::StdRng::seed_from_u64(config.seed);
145
146 let random_signs: Vec<i8> = (0..dim)
148 .map(|_| if rng.random::<bool>() { 1 } else { -1 })
149 .collect();
150
151 let mut random_perm: Vec<u32> = (0..dim as u32).collect();
153 for i in (1..dim).rev() {
154 let j = rng.random_range(0..=i);
155 random_perm.swap(i, j);
156 }
157
158 let version = config.seed
164 ^ (config.dim as u64).wrapping_mul(0x9e3779b97f4a7c15)
165 ^ (config.ex_bits as u64).wrapping_mul(0xd6e8_feb8_6659_fd93);
166
167 Self {
168 config,
169 random_signs,
170 random_perm,
171 version,
172 }
173 }
174
175 pub fn encode(&self, vector: &[f32], centroid: Option<&[f32]>) -> QuantizedVector {
179 let dim = self.config.dim;
180
181 let mut normalized: Vec<f32> = if let Some(c) = centroid {
183 vector.iter().zip(c).map(|(&v, &c)| v - c).collect()
184 } else {
185 vector.to_vec()
186 };
187
188 let norm: f32 = normalized.iter().map(|x| x * x).sum::<f32>().sqrt();
189 let dist_to_centroid = norm;
190
191 if norm > 1e-10 {
192 let inv_norm = 1.0 / norm;
193 for x in normalized.iter_mut() {
194 *x *= inv_norm;
195 }
196 }
197
198 let transformed: Vec<f32> = (0..dim)
200 .map(|i| {
201 let src_idx = self.random_perm[i] as usize;
202 normalized[src_idx] * self.random_signs[src_idx] as f32
203 })
204 .collect();
205
206 let num_bytes = dim.div_ceil(8);
208 let mut bits = vec![0u8; num_bytes];
209 let mut popcount = 0u32;
210
211 for i in 0..dim {
212 if transformed[i] >= 0.0 {
213 bits[i / 8] |= 1 << (i % 8);
214 popcount += 1;
215 }
216 }
217
218 let scale = 1.0 / (dim as f32).sqrt();
220 let mut self_dot = 0.0f32;
221 for i in 0..dim {
222 let o_bar_i = if (bits[i / 8] >> (i % 8)) & 1 == 1 {
223 scale
224 } else {
225 -scale
226 };
227 self_dot += transformed[i] * o_bar_i;
228 }
229
230 let (ex_code, ex_scale, ex_norm) = if self.config.ex_bits > 0 {
233 let ex_bits = self.config.ex_bits as u32;
234 let levels = 1u32 << ex_bits;
235 let max_abs = transformed.iter().fold(0.0f32, |m, &x| m.max(x.abs()));
236 let ex_scale = if max_abs > 1e-10 { max_abs } else { 1.0 };
237 let step = ex_scale / levels as f32;
238
239 let total_bits = dim * ex_bits as usize;
240 let mut ex_code = vec![0u8; total_bits.div_ceil(8)];
241 let mut norm_sq = 0.0f64;
242 let mut bit_pos = 0usize;
243 for &t in &transformed {
244 let mag = (t.abs() / step) as u32;
245 let code = mag.min(levels - 1);
246 let mut v = code;
248 let mut remaining = ex_bits as usize;
249 let mut pos = bit_pos;
250 while remaining > 0 {
251 let byte = pos / 8;
252 let offset = pos % 8;
253 let take = remaining.min(8 - offset);
254 ex_code[byte] |= ((v & ((1 << take) - 1)) as u8) << offset;
255 v >>= take;
256 pos += take;
257 remaining -= take;
258 }
259 bit_pos += ex_bits as usize;
260
261 let recon = (code as f32 + 0.5) * step;
262 norm_sq += (recon as f64) * (recon as f64);
263 }
264
265 (ex_code, ex_scale, (norm_sq.sqrt()) as f32)
266 } else {
267 (Vec::new(), 0.0, 0.0)
268 };
269
270 QuantizedVector {
271 bits,
272 dist_to_centroid,
273 self_dot,
274 popcount,
275 ex_code,
276 ex_scale,
277 ex_norm,
278 }
279 }
280
281 pub fn prepare_query(&self, query: &[f32], centroid: Option<&[f32]>) -> QuantizedQuery {
283 let dim = self.config.dim;
284
285 let mut normalized: Vec<f32> = if let Some(c) = centroid {
287 query.iter().zip(c).map(|(&v, &c)| v - c).collect()
288 } else {
289 query.to_vec()
290 };
291
292 let norm: f32 = normalized.iter().map(|x| x * x).sum::<f32>().sqrt();
293 let dist_to_centroid = norm;
294
295 if norm > 1e-10 {
296 let inv_norm = 1.0 / norm;
297 for x in normalized.iter_mut() {
298 *x *= inv_norm;
299 }
300 }
301
302 let transformed: Vec<f32> = (0..dim)
304 .map(|i| {
305 let src_idx = self.random_perm[i] as usize;
306 normalized[src_idx] * self.random_signs[src_idx] as f32
307 })
308 .collect();
309
310 let min_val = transformed.iter().cloned().fold(f32::INFINITY, f32::min);
312 let max_val = transformed
313 .iter()
314 .cloned()
315 .fold(f32::NEG_INFINITY, f32::max);
316 let lower = min_val;
317 let width = if max_val > min_val {
318 max_val - min_val
319 } else {
320 1.0
321 };
322
323 let quantized_vals: Vec<u8> = transformed
325 .iter()
326 .map(|&x| {
327 let normalized = (x - lower) / width;
328 (normalized * 15.0).round().clamp(0.0, 15.0) as u8
329 })
330 .collect();
331
332 let num_bytes = dim.div_ceil(2);
334 let mut quantized = vec![0u8; num_bytes];
335 for i in 0..dim {
336 if i % 2 == 0 {
337 quantized[i / 2] |= quantized_vals[i];
338 } else {
339 quantized[i / 2] |= quantized_vals[i] << 4;
340 }
341 }
342
343 let sum: u32 = quantized_vals.iter().map(|&x| x as u32).sum();
345
346 let num_luts = dim.div_ceil(4);
348 let mut luts = vec![[0u16; 16]; num_luts];
349
350 for (lut_idx, lut) in luts.iter_mut().enumerate() {
351 let base_dim = lut_idx * 4;
352 for pattern in 0u8..16 {
353 let mut dot = 0u16;
354 for bit in 0..4 {
355 let dim_idx = base_dim + bit;
356 if dim_idx < dim && (pattern >> bit) & 1 == 1 {
357 dot += quantized_vals[dim_idx] as u16;
358 }
359 }
360 lut[pattern as usize] = dot;
361 }
362 }
363
364 QuantizedQuery {
365 quantized,
366 dist_to_centroid,
367 lower,
368 width,
369 sum,
370 luts,
371 transformed,
372 }
373 }
374
375 pub fn estimate_distance(&self, query: &QuantizedQuery, code: &QuantizedVector) -> f32 {
377 if !code.ex_code.is_empty() {
379 return self.estimate_distance_extended(query, code);
380 }
381
382 let dim = self.config.dim;
383
384 let dot_sum = lut_dot_product_simd(&code.bits, &query.luts);
386
387 let scale = 1.0 / (dim as f32).sqrt();
388
389 let sum_positive = code.popcount as f32 * query.lower + dot_sum as f32 * query.width / 15.0;
391 let sum_all = dim as f32 * query.lower + query.sum as f32 * query.width / 15.0;
392
393 let q_obar_dot = scale * (2.0 * sum_positive - sum_all);
395
396 let q_o_estimate = if code.self_dot.abs() > 1e-6 {
398 q_obar_dot / code.self_dot
399 } else {
400 q_obar_dot
401 };
402
403 let q_o_clamped = q_o_estimate.clamp(-1.0, 1.0);
405
406 let dist_sq = code.dist_to_centroid * code.dist_to_centroid
408 + query.dist_to_centroid * query.dist_to_centroid
409 - 2.0 * code.dist_to_centroid * query.dist_to_centroid * q_o_clamped;
410
411 dist_sq.max(0.0)
412 }
413
414 fn estimate_distance_extended(&self, query: &QuantizedQuery, code: &QuantizedVector) -> f32 {
421 let dim = self.config.dim;
422 let ex_bits = self.config.ex_bits as usize;
423 debug_assert!(ex_bits > 0 && ex_bits <= 8);
424 let levels = 1u32 << ex_bits;
425 let step = code.ex_scale / levels as f32;
426 let mask = levels - 1;
427
428 let mut acc: u64 = 0;
430 let mut acc_bits: usize = 0;
431 let mut byte_idx: usize = 0;
432 let ex_code = &code.ex_code;
433
434 let mut dot = 0.0f32;
435 for (i, &q) in query.transformed.iter().enumerate().take(dim) {
436 while acc_bits < ex_bits {
437 acc |= (ex_code.get(byte_idx).copied().unwrap_or(0) as u64) << acc_bits;
438 byte_idx += 1;
439 acc_bits += 8;
440 }
441 let mag_code = (acc as u32) & mask;
442 acc >>= ex_bits;
443 acc_bits -= ex_bits;
444
445 let magnitude = (mag_code as f32 + 0.5) * step;
446 let signed = if (code.bits[i / 8] >> (i % 8)) & 1 == 1 {
447 magnitude
448 } else {
449 -magnitude
450 };
451 dot += q * signed;
452 }
453
454 let q_o_estimate = if code.ex_norm > 1e-10 {
456 dot / code.ex_norm
457 } else {
458 dot
459 };
460 let q_o_clamped = q_o_estimate.clamp(-1.0, 1.0);
461
462 let dist_sq = code.dist_to_centroid * code.dist_to_centroid
463 + query.dist_to_centroid * query.dist_to_centroid
464 - 2.0 * code.dist_to_centroid * query.dist_to_centroid * q_o_clamped;
465
466 dist_sq.max(0.0)
467 }
468
469 pub fn size_bytes(&self) -> usize {
471 self.random_signs.len() + self.random_perm.len() * 4 + 64
472 }
473
474 pub fn estimated_memory_bytes(&self) -> usize {
476 self.size_bytes()
477 }
478}
479
480impl Quantizer for RaBitQCodebook {
481 type Code = QuantizedVector;
482 type Config = RaBitQConfig;
483 type QueryData = QuantizedQuery;
484
485 fn encode(&self, vector: &[f32], centroid: Option<&[f32]>) -> Self::Code {
486 self.encode(vector, centroid)
487 }
488
489 fn prepare_query(&self, query: &[f32], centroid: Option<&[f32]>) -> Self::QueryData {
490 self.prepare_query(query, centroid)
491 }
492
493 fn compute_distance(&self, query_data: &Self::QueryData, code: &Self::Code) -> f32 {
494 self.estimate_distance(query_data, code)
495 }
496
497 fn size_bytes(&self) -> usize {
498 self.size_bytes()
499 }
500}
501
502#[inline]
508fn lut_dot_product_simd(bits: &[u8], luts: &[[u16; 16]]) -> u32 {
509 #[cfg(target_arch = "aarch64")]
510 {
511 if let Some(result) = lut_dot_product_neon(bits, luts) {
512 return result;
513 }
514 }
515
516 #[cfg(target_arch = "x86_64")]
517 {
518 if is_x86_feature_detected!("ssse3") {
519 unsafe {
520 if let Some(result) = lut_dot_product_ssse3(bits, luts) {
521 return result;
522 }
523 }
524 }
525 }
526
527 lut_dot_product_scalar(bits, luts)
528}
529
530#[inline]
532fn lut_dot_product_scalar(bits: &[u8], luts: &[[u16; 16]]) -> u32 {
533 let mut dot_sum = 0u32;
534
535 for (lut_idx, lut) in luts.iter().enumerate() {
536 let base_bit = lut_idx * 4;
537 let byte_idx = base_bit / 8;
538 let bit_offset = base_bit % 8;
539
540 let byte = bits.get(byte_idx).copied().unwrap_or(0);
541 let next_byte = bits.get(byte_idx + 1).copied().unwrap_or(0);
542
543 let pattern = if bit_offset <= 4 {
544 (byte >> bit_offset) & 0x0F
545 } else {
546 ((byte >> bit_offset) | (next_byte << (8 - bit_offset))) & 0x0F
547 };
548
549 dot_sum += lut[pattern as usize] as u32;
550 }
551
552 dot_sum
553}
554
555#[cfg(target_arch = "aarch64")]
557#[inline]
558fn lut_dot_product_neon(bits: &[u8], luts: &[[u16; 16]]) -> Option<u32> {
559 if luts.len() < 8 {
560 return None;
561 }
562
563 let mut total = 0u32;
564 let num_luts = luts.len();
565 let mut lut_idx = 0;
566
567 while lut_idx + 2 <= num_luts {
568 let base_bit0 = lut_idx * 4;
569 let base_bit1 = (lut_idx + 1) * 4;
570
571 let byte_idx0 = base_bit0 / 8;
572 let bit_offset0 = base_bit0 % 8;
573 let byte_idx1 = base_bit1 / 8;
574 let bit_offset1 = base_bit1 % 8;
575
576 let byte0 = bits.get(byte_idx0).copied().unwrap_or(0);
577 let next0 = bits.get(byte_idx0 + 1).copied().unwrap_or(0);
578 let byte1 = bits.get(byte_idx1).copied().unwrap_or(0);
579 let next1 = bits.get(byte_idx1 + 1).copied().unwrap_or(0);
580
581 let pattern0 = if bit_offset0 <= 4 {
582 (byte0 >> bit_offset0) & 0x0F
583 } else {
584 ((byte0 >> bit_offset0) | (next0 << (8 - bit_offset0))) & 0x0F
585 };
586
587 let pattern1 = if bit_offset1 <= 4 {
588 (byte1 >> bit_offset1) & 0x0F
589 } else {
590 ((byte1 >> bit_offset1) | (next1 << (8 - bit_offset1))) & 0x0F
591 };
592
593 total += luts[lut_idx][pattern0 as usize] as u32;
594 total += luts[lut_idx + 1][pattern1 as usize] as u32;
595
596 lut_idx += 2;
597 }
598
599 while lut_idx < num_luts {
600 let base_bit = lut_idx * 4;
601 let byte_idx = base_bit / 8;
602 let bit_offset = base_bit % 8;
603
604 let byte = bits.get(byte_idx).copied().unwrap_or(0);
605 let next_byte = bits.get(byte_idx + 1).copied().unwrap_or(0);
606
607 let pattern = if bit_offset <= 4 {
608 (byte >> bit_offset) & 0x0F
609 } else {
610 ((byte >> bit_offset) | (next_byte << (8 - bit_offset))) & 0x0F
611 };
612
613 total += luts[lut_idx][pattern as usize] as u32;
614 lut_idx += 1;
615 }
616
617 Some(total)
618}
619
620#[cfg(target_arch = "x86_64")]
622#[target_feature(enable = "ssse3")]
623#[inline]
624unsafe fn lut_dot_product_ssse3(bits: &[u8], luts: &[[u16; 16]]) -> Option<u32> {
625 if luts.len() < 8 {
626 return None;
627 }
628 Some(lut_dot_product_scalar(bits, luts))
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634
635 #[test]
636 fn test_rabitq_codebook_basic() {
637 let config = RaBitQConfig::new(128);
638 let codebook = RaBitQCodebook::new(config);
639
640 assert_eq!(codebook.random_signs.len(), 128);
641 assert_eq!(codebook.random_perm.len(), 128);
642 }
643
644 #[test]
645 fn test_encode_decode() {
646 let config = RaBitQConfig::new(64);
647 let codebook = RaBitQCodebook::new(config);
648
649 let vector: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) / 32.0).collect();
650 let code = codebook.encode(&vector, None);
651
652 assert_eq!(code.bits.len(), 8); assert!(code.dist_to_centroid > 0.0);
654 }
655
656 #[test]
657 fn test_distance_estimation() {
658 let config = RaBitQConfig::new(64);
659 let codebook = RaBitQCodebook::new(config);
660
661 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
662 let v1: Vec<f32> = (0..64).map(|_| rng.random::<f32>() - 0.5).collect();
663 let v2: Vec<f32> = (0..64).map(|_| rng.random::<f32>() - 0.5).collect();
664
665 let code = codebook.encode(&v1, None);
666 let query = codebook.prepare_query(&v2, None);
667
668 let estimated = codebook.estimate_distance(&query, &code);
669 assert!(estimated >= 0.0);
670 }
671
672 #[test]
673 fn test_extended_bits_reduce_estimation_error() {
674 let dim = 128;
675 let n = 200;
676 let mut rng = rand::rngs::StdRng::seed_from_u64(7);
677 let vectors: Vec<Vec<f32>> = (0..n)
678 .map(|_| (0..dim).map(|_| rng.random::<f32>() - 0.5).collect())
679 .collect();
680 let query: Vec<f32> = (0..dim).map(|_| rng.random::<f32>() - 0.5).collect();
681
682 let exact =
683 |a: &[f32], b: &[f32]| -> f32 { a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum() };
684
685 let mut errors = Vec::new();
686 for bits in [1u8, 5u8] {
687 let config = RaBitQConfig::new(dim).with_bits(bits);
688 let codebook = RaBitQCodebook::new(config);
689 let q = codebook.prepare_query(&query, None);
690
691 let mut total_err = 0.0f64;
692 for v in &vectors {
693 let code = codebook.encode(v, None);
694 let est = codebook.estimate_distance(&q, &code);
695 let truth = exact(&query, v);
696 total_err += ((est - truth).abs() / truth.max(1e-6)) as f64;
697 }
698 errors.push(total_err / n as f64);
699 }
700
701 assert!(
703 errors[1] < errors[0] * 0.5,
704 "extended codes should at least halve mean relative error: 1-bit={:.4}, 5-bit={:.4}",
705 errors[0],
706 errors[1]
707 );
708 assert!(
710 errors[1] < 0.05,
711 "5-bit mean relative error should be <5%, got {:.4}",
712 errors[1]
713 );
714 }
715
716 #[test]
717 fn test_extended_code_serde_roundtrip_and_legacy() {
718 let dim = 64;
719 let config = RaBitQConfig::new(dim).with_bits(4);
720 let codebook = RaBitQCodebook::new(config);
721 let v: Vec<f32> = (0..dim).map(|i| (i as f32 - 32.0) / 32.0).collect();
722 let code = codebook.encode(&v, None);
723 assert!(!code.ex_code.is_empty());
724 assert!(code.ex_norm > 0.0);
725
726 let json = serde_json::to_vec(&code).unwrap();
728 let back: QuantizedVector = serde_json::from_slice(&json).unwrap();
729 assert_eq!(back.ex_code, code.ex_code);
730 assert_eq!(back.ex_scale, code.ex_scale);
731
732 let legacy = serde_json::json!({
734 "bits": code.bits,
735 "dist_to_centroid": code.dist_to_centroid,
736 "self_dot": code.self_dot,
737 "popcount": code.popcount,
738 });
739 let old: QuantizedVector = serde_json::from_value(legacy).unwrap();
740 assert!(old.ex_code.is_empty());
741 }
742
743 #[test]
744 fn test_extended_version_differs_from_classic() {
745 let dim = 64;
746 let classic = RaBitQCodebook::new(RaBitQConfig::new(dim));
747 let extended = RaBitQCodebook::new(RaBitQConfig::new(dim).with_bits(4));
748 assert_ne!(
749 classic.version, extended.version,
750 "1-bit and multi-bit segments must not be merge-compatible"
751 );
752 }
753
754 #[test]
755 fn test_quantizer_trait() {
756 let config = RaBitQConfig::new(32);
757 let codebook = RaBitQCodebook::new(config);
758
759 let vector: Vec<f32> = (0..32).map(|i| i as f32 / 32.0).collect();
760 let query: Vec<f32> = (0..32).map(|i| (31 - i) as f32 / 32.0).collect();
761
762 let code = Quantizer::encode(&codebook, &vector, None);
764 let query_data = Quantizer::prepare_query(&codebook, &query, None);
765 let dist = Quantizer::compute_distance(&codebook, &query_data, &code);
766
767 assert!(dist >= 0.0);
768 }
769}