| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| use crate::distance::{fvec_norm_l2sqr, MetricType}; |
| |
| pub const DEFAULT_RQ_ROTATION_SEED: u64 = 0x9E37_79B9_7F4A_7C15; |
| pub const DEFAULT_RQ_ROTATION_ROUNDS: u32 = 3; |
| pub const RQ_BYTE_LUT_MIN_LIST_SIZE: usize = 64; |
| pub const DEFAULT_RQ_QUERY_BITS: usize = 0; |
| |
| #[derive(Debug, Clone, Copy, PartialEq)] |
| pub struct RQCodeFactors { |
| pub residual_norm_sqr: f32, |
| pub vector_norm_sqr: f32, |
| pub dp_multiplier: f32, |
| } |
| |
| impl RQCodeFactors { |
| pub fn zero() -> Self { |
| Self { |
| residual_norm_sqr: 0.0, |
| vector_norm_sqr: 0.0, |
| dp_multiplier: 0.0, |
| } |
| } |
| } |
| |
| #[derive(Debug, Clone)] |
| pub struct RQRotation { |
| d: usize, |
| seed: u64, |
| rounds: u32, |
| ops: Vec<KacOp>, |
| } |
| |
| #[derive(Debug, Clone, Copy)] |
| struct KacOp { |
| i: usize, |
| j: usize, |
| cos: f32, |
| sin: f32, |
| } |
| |
| impl RQRotation { |
| pub fn new(d: usize, seed: u64, rounds: u32) -> Self { |
| let mut rng = SplitMix64::new(seed ^ (d as u64).rotate_left(17)); |
| let mut ops = Vec::new(); |
| if d >= 2 { |
| for _ in 0..rounds { |
| let mut order: Vec<usize> = (0..d).collect(); |
| for i in (1..d).rev() { |
| let j = rng.next_usize(i + 1); |
| order.swap(i, j); |
| } |
| for pair in order.chunks_exact(2) { |
| let angle = (rng.next_f32() * 2.0 - 1.0) * std::f32::consts::PI; |
| let (sin, cos) = angle.sin_cos(); |
| ops.push(KacOp { |
| i: pair[0], |
| j: pair[1], |
| cos, |
| sin, |
| }); |
| } |
| } |
| } |
| Self { |
| d, |
| seed, |
| rounds, |
| ops, |
| } |
| } |
| |
| pub fn seed(&self) -> u64 { |
| self.seed |
| } |
| |
| pub fn rounds(&self) -> u32 { |
| self.rounds |
| } |
| |
| pub fn apply(&self, values: &mut [f32]) { |
| debug_assert_eq!(values.len(), self.d); |
| for op in &self.ops { |
| let x = values[op.i]; |
| let y = values[op.j]; |
| values[op.i] = op.cos * x - op.sin * y; |
| values[op.j] = op.sin * x + op.cos * y; |
| } |
| } |
| } |
| |
| #[derive(Debug, Clone)] |
| pub struct RaBitQuantizer { |
| d: usize, |
| inv_sqrt_d: f32, |
| } |
| |
| #[derive(Debug, Clone)] |
| pub struct RQDistanceContext { |
| d: usize, |
| code_size: usize, |
| rotated_query_residual: Vec<f32>, |
| query_residual_norm_sqr: f32, |
| query_norm_sqr: f32, |
| byte_signed_sums: Option<Vec<f32>>, |
| quantized_query: Option<RQQuantizedQuery>, |
| } |
| |
| #[derive(Debug, Clone)] |
| struct RQQuantizedQuery { |
| scale: f32, |
| sign_bits: Vec<u8>, |
| magnitude_bit_planes: Vec<Vec<u8>>, |
| } |
| |
| impl RaBitQuantizer { |
| pub fn new(d: usize) -> Self { |
| let inv_sqrt_d = if d == 0 { 1.0 } else { 1.0 / (d as f32).sqrt() }; |
| Self { d, inv_sqrt_d } |
| } |
| |
| pub fn code_size(&self) -> usize { |
| self.d.div_ceil(8) |
| } |
| |
| pub fn encode( |
| &self, |
| rotated_residual: &[f32], |
| vector_norm_sqr: f32, |
| code: &mut [u8], |
| ) -> RQCodeFactors { |
| debug_assert_eq!(rotated_residual.len(), self.d); |
| debug_assert!(code.len() >= self.code_size()); |
| code[..self.code_size()].fill(0); |
| |
| let (residual_norm_sqr, abs_sum) = fvec_norm_l2sqr_abs_sum(rotated_residual); |
| for (byte_idx, chunk) in rotated_residual.chunks(8).enumerate() { |
| let mut byte = 0u8; |
| for (bit, &value) in chunk.iter().enumerate() { |
| if value > 0.0 { |
| byte |= 1u8 << bit; |
| } |
| } |
| code[byte_idx] = byte; |
| } |
| |
| let dp_multiplier = if abs_sum > f32::EPSILON { |
| residual_norm_sqr / (abs_sum * self.inv_sqrt_d) |
| } else { |
| 0.0 |
| }; |
| RQCodeFactors { |
| residual_norm_sqr, |
| vector_norm_sqr, |
| dp_multiplier, |
| } |
| } |
| |
| pub fn distance_to_code( |
| &self, |
| rotated_query_residual: &[f32], |
| query: &[f32], |
| code: &[u8], |
| factors: RQCodeFactors, |
| metric: MetricType, |
| ) -> f32 { |
| debug_assert_eq!(rotated_query_residual.len(), self.d); |
| debug_assert_eq!(query.len(), self.d); |
| |
| let context = self.prepare_distance_context(rotated_query_residual.to_vec(), query, false); |
| self.distance_to_code_prepared(&context, code, factors, metric) |
| } |
| |
| pub fn prepare_distance_context( |
| &self, |
| rotated_query_residual: Vec<f32>, |
| query: &[f32], |
| build_byte_lut: bool, |
| ) -> RQDistanceContext { |
| self.prepare_distance_context_with_query_bits( |
| rotated_query_residual, |
| query, |
| build_byte_lut, |
| DEFAULT_RQ_QUERY_BITS, |
| ) |
| } |
| |
| pub fn prepare_distance_context_with_query_bits( |
| &self, |
| rotated_query_residual: Vec<f32>, |
| query: &[f32], |
| build_byte_lut: bool, |
| query_bits: usize, |
| ) -> RQDistanceContext { |
| debug_assert_eq!(rotated_query_residual.len(), self.d); |
| debug_assert_eq!(query.len(), self.d); |
| assert!( |
| is_supported_query_bits(query_bits), |
| "unsupported IVF-RQ query_bits {}; expected 0, 4, or 8", |
| query_bits |
| ); |
| |
| let query_residual_norm_sqr = fvec_norm_l2sqr(&rotated_query_residual); |
| let query_norm_sqr = fvec_norm_l2sqr(query); |
| let quantized_query = if query_bits == DEFAULT_RQ_QUERY_BITS { |
| None |
| } else { |
| Some(self.quantize_query(&rotated_query_residual, query_bits)) |
| }; |
| let byte_signed_sums = if quantized_query.is_none() && build_byte_lut { |
| Some(self.build_byte_signed_sums(&rotated_query_residual)) |
| } else { |
| None |
| }; |
| |
| RQDistanceContext { |
| d: self.d, |
| code_size: self.code_size(), |
| rotated_query_residual, |
| query_residual_norm_sqr, |
| query_norm_sqr, |
| byte_signed_sums, |
| quantized_query, |
| } |
| } |
| |
| pub fn distance_to_code_prepared( |
| &self, |
| context: &RQDistanceContext, |
| code: &[u8], |
| factors: RQCodeFactors, |
| metric: MetricType, |
| ) -> f32 { |
| debug_assert_eq!(context.d, self.d); |
| debug_assert!(code.len() >= context.code_size); |
| |
| let signed_query_sum = self.signed_query_sum(context, code); |
| let approx_ip = factors.dp_multiplier * signed_query_sum * self.inv_sqrt_d; |
| let approx_l2 = (factors.residual_norm_sqr + context.query_residual_norm_sqr |
| - 2.0 * approx_ip) |
| .max(0.0); |
| |
| match metric { |
| MetricType::L2 => approx_l2, |
| MetricType::Cosine => 0.5 * approx_l2, |
| MetricType::InnerProduct => { |
| let base = factors.residual_norm_sqr - factors.vector_norm_sqr; |
| let pre_dist = base + context.query_residual_norm_sqr - 2.0 * approx_ip; |
| 0.5 * (pre_dist - context.query_norm_sqr) |
| } |
| } |
| } |
| |
| fn signed_query_sum(&self, context: &RQDistanceContext, code: &[u8]) -> f32 { |
| if let Some(quantized_query) = &context.quantized_query { |
| return quantized_query.signed_query_sum(code, context.code_size); |
| } |
| |
| if let Some(byte_signed_sums) = &context.byte_signed_sums { |
| let mut sum = 0.0f32; |
| for byte_idx in 0..context.code_size { |
| sum += byte_signed_sums[byte_idx * 256 + code[byte_idx] as usize]; |
| } |
| return sum; |
| } |
| |
| let mut sum = 0.0f32; |
| for (dim, &value) in context.rotated_query_residual.iter().enumerate() { |
| sum += if get_bit(code, dim) { value } else { -value }; |
| } |
| sum |
| } |
| |
| fn quantize_query( |
| &self, |
| rotated_query_residual: &[f32], |
| query_bits: usize, |
| ) -> RQQuantizedQuery { |
| let magnitude_bits = query_bits - 1; |
| let max_level = (1usize << magnitude_bits) - 1; |
| let code_size = self.code_size(); |
| let max_abs = rotated_query_residual |
| .iter() |
| .map(|value| value.abs()) |
| .fold(0.0f32, f32::max); |
| let scale = if max_abs > f32::EPSILON { |
| max_abs / max_level as f32 |
| } else { |
| 0.0 |
| }; |
| let mut sign_bits = vec![0u8; code_size]; |
| let mut magnitude_bit_planes = vec![vec![0u8; code_size]; magnitude_bits]; |
| |
| if scale == 0.0 { |
| return RQQuantizedQuery { |
| scale, |
| sign_bits, |
| magnitude_bit_planes, |
| }; |
| } |
| |
| for (dim, &value) in rotated_query_residual.iter().enumerate() { |
| if value >= 0.0 { |
| sign_bits[dim / 8] |= 1u8 << (dim % 8); |
| } |
| let level = (value.abs() / scale).round().clamp(0.0, max_level as f32) as usize; |
| for (bit, plane) in magnitude_bit_planes.iter_mut().enumerate() { |
| if (level >> bit) & 1 != 0 { |
| plane[dim / 8] |= 1u8 << (dim % 8); |
| } |
| } |
| } |
| |
| RQQuantizedQuery { |
| scale, |
| sign_bits, |
| magnitude_bit_planes, |
| } |
| } |
| |
| fn build_byte_signed_sums(&self, rotated_query_residual: &[f32]) -> Vec<f32> { |
| let code_size = self.code_size(); |
| let mut byte_signed_sums = vec![0.0f32; code_size * 256]; |
| for byte_idx in 0..code_size { |
| let dim_base = byte_idx * 8; |
| let dim_end = (dim_base + 8).min(self.d); |
| let lut = &mut byte_signed_sums[byte_idx * 256..(byte_idx + 1) * 256]; |
| lut[0] = -rotated_query_residual[dim_base..dim_end] |
| .iter() |
| .sum::<f32>(); |
| for pattern in 1..256usize { |
| let bit = pattern.trailing_zeros() as usize; |
| let previous = pattern & (pattern - 1); |
| let value = if dim_base + bit < dim_end { |
| rotated_query_residual[dim_base + bit] |
| } else { |
| 0.0 |
| }; |
| lut[pattern] = lut[previous] + 2.0 * value; |
| } |
| } |
| byte_signed_sums |
| } |
| } |
| |
| impl RQQuantizedQuery { |
| fn signed_query_sum(&self, code: &[u8], code_size: usize) -> f32 { |
| if self.scale == 0.0 { |
| return 0.0; |
| } |
| |
| let mut signed_level_sum = 0i64; |
| for (bit, plane) in self.magnitude_bit_planes.iter().enumerate() { |
| let weight = 1i64 << bit; |
| let mut plane_sum = 0i64; |
| let mut offset = 0usize; |
| |
| while offset + 8 <= code_size { |
| let selected = u64::from_le_bytes(plane[offset..offset + 8].try_into().unwrap()); |
| if selected != 0 { |
| let code_bits = |
| u64::from_le_bytes(code[offset..offset + 8].try_into().unwrap()); |
| let sign_bits = |
| u64::from_le_bytes(self.sign_bits[offset..offset + 8].try_into().unwrap()); |
| let same_sign = !(code_bits ^ sign_bits) & selected; |
| plane_sum += 2 * same_sign.count_ones() as i64 - selected.count_ones() as i64; |
| } |
| offset += 8; |
| } |
| |
| while offset < code_size { |
| let selected = plane[offset]; |
| if selected != 0 { |
| let same_sign = !(code[offset] ^ self.sign_bits[offset]) & selected; |
| plane_sum += 2 * same_sign.count_ones() as i64 - selected.count_ones() as i64; |
| } |
| offset += 1; |
| } |
| |
| signed_level_sum += weight * plane_sum; |
| } |
| |
| self.scale * signed_level_sum as f32 |
| } |
| } |
| |
| #[inline] |
| pub fn is_supported_query_bits(query_bits: usize) -> bool { |
| matches!(query_bits, 0 | 4 | 8) |
| } |
| |
| fn get_bit(code: &[u8], dim: usize) -> bool { |
| code[dim / 8] & (1u8 << (dim % 8)) != 0 |
| } |
| |
| #[cfg(target_arch = "x86_64")] |
| #[inline] |
| fn fvec_norm_l2sqr_abs_sum(values: &[f32]) -> (f32, f32) { |
| if is_x86_feature_detected!("avx2") && values.len() >= 8 { |
| unsafe { fvec_norm_l2sqr_abs_sum_avx2(values) } |
| } else { |
| fvec_norm_l2sqr_abs_sum_scalar(values) |
| } |
| } |
| |
| #[cfg(target_arch = "aarch64")] |
| #[inline] |
| fn fvec_norm_l2sqr_abs_sum(values: &[f32]) -> (f32, f32) { |
| unsafe { fvec_norm_l2sqr_abs_sum_neon(values) } |
| } |
| |
| #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] |
| #[inline] |
| fn fvec_norm_l2sqr_abs_sum(values: &[f32]) -> (f32, f32) { |
| fvec_norm_l2sqr_abs_sum_scalar(values) |
| } |
| |
| #[cfg(any( |
| target_arch = "x86_64", |
| not(any(target_arch = "x86_64", target_arch = "aarch64")) |
| ))] |
| #[inline] |
| fn fvec_norm_l2sqr_abs_sum_scalar(values: &[f32]) -> (f32, f32) { |
| let mut norm = 0.0f32; |
| let mut abs_sum = 0.0f32; |
| for &value in values { |
| norm += value * value; |
| abs_sum += value.abs(); |
| } |
| (norm, abs_sum) |
| } |
| |
| #[cfg(target_arch = "x86_64")] |
| #[target_feature(enable = "avx2")] |
| unsafe fn fvec_norm_l2sqr_abs_sum_avx2(values: &[f32]) -> (f32, f32) { |
| use std::arch::x86_64::*; |
| |
| let n = values.len(); |
| let abs_mask = _mm256_castsi256_ps(_mm256_set1_epi32(0x7fff_ffff)); |
| let mut norm_sum = _mm256_setzero_ps(); |
| let mut abs_sum_vec = _mm256_setzero_ps(); |
| let mut i = 0; |
| while i + 8 <= n { |
| let value = unsafe { _mm256_loadu_ps(values.as_ptr().add(i)) }; |
| norm_sum = _mm256_add_ps(norm_sum, _mm256_mul_ps(value, value)); |
| abs_sum_vec = _mm256_add_ps(abs_sum_vec, _mm256_and_ps(value, abs_mask)); |
| i += 8; |
| } |
| |
| let norm_hi = _mm256_extractf128_ps::<1>(norm_sum); |
| let norm_lo = _mm256_castps256_ps128(norm_sum); |
| let norm_128 = _mm_add_ps(norm_lo, norm_hi); |
| let norm_64 = _mm_add_ps(norm_128, _mm_movehl_ps(norm_128, norm_128)); |
| let norm_32 = _mm_add_ss(norm_64, _mm_shuffle_ps::<1>(norm_64, norm_64)); |
| let mut norm = _mm_cvtss_f32(norm_32); |
| |
| let abs_hi = _mm256_extractf128_ps::<1>(abs_sum_vec); |
| let abs_lo = _mm256_castps256_ps128(abs_sum_vec); |
| let abs_128 = _mm_add_ps(abs_lo, abs_hi); |
| let abs_64 = _mm_add_ps(abs_128, _mm_movehl_ps(abs_128, abs_128)); |
| let abs_32 = _mm_add_ss(abs_64, _mm_shuffle_ps::<1>(abs_64, abs_64)); |
| let mut abs_sum = _mm_cvtss_f32(abs_32); |
| |
| while i < n { |
| let value = unsafe { *values.get_unchecked(i) }; |
| norm += value * value; |
| abs_sum += value.abs(); |
| i += 1; |
| } |
| (norm, abs_sum) |
| } |
| |
| #[cfg(target_arch = "aarch64")] |
| #[target_feature(enable = "neon")] |
| unsafe fn fvec_norm_l2sqr_abs_sum_neon(values: &[f32]) -> (f32, f32) { |
| use std::arch::aarch64::*; |
| |
| let n = values.len(); |
| let mut norm_sum = vdupq_n_f32(0.0); |
| let mut abs_sum_vec = vdupq_n_f32(0.0); |
| let mut i = 0; |
| while i + 4 <= n { |
| let value = unsafe { vld1q_f32(values.as_ptr().add(i)) }; |
| norm_sum = vmlaq_f32(norm_sum, value, value); |
| abs_sum_vec = vaddq_f32(abs_sum_vec, vabsq_f32(value)); |
| i += 4; |
| } |
| |
| let mut norm = vaddvq_f32(norm_sum); |
| let mut abs_sum = vaddvq_f32(abs_sum_vec); |
| while i < n { |
| let value = unsafe { *values.get_unchecked(i) }; |
| norm += value * value; |
| abs_sum += value.abs(); |
| i += 1; |
| } |
| (norm, abs_sum) |
| } |
| |
| #[derive(Debug, Clone, Copy)] |
| struct SplitMix64 { |
| state: u64, |
| } |
| |
| impl SplitMix64 { |
| fn new(seed: u64) -> Self { |
| Self { state: seed } |
| } |
| |
| fn next_u64(&mut self) -> u64 { |
| self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15); |
| let mut z = self.state; |
| z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); |
| z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); |
| z ^ (z >> 31) |
| } |
| |
| fn next_usize(&mut self, upper: usize) -> usize { |
| (self.next_u64() % upper as u64) as usize |
| } |
| |
| fn next_f32(&mut self) -> f32 { |
| let mantissa = (self.next_u64() >> 40) as u32; |
| mantissa as f32 / ((1u32 << 24) as f32) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn rabit_quantizer_estimates_self_distance_as_zero() { |
| let d = 8; |
| let rotation = RQRotation::new(d, DEFAULT_RQ_ROTATION_SEED, DEFAULT_RQ_ROTATION_ROUNDS); |
| let quantizer = RaBitQuantizer::new(d); |
| let centroid = vec![1.0; d]; |
| let vector = vec![2.0, 1.5, 0.75, 3.0, 1.25, -1.0, 4.0, 2.5]; |
| let mut residual: Vec<f32> = vector |
| .iter() |
| .zip(centroid.iter()) |
| .map(|(&x, &c)| x - c) |
| .collect(); |
| rotation.apply(&mut residual); |
| |
| let mut code = vec![0u8; quantizer.code_size()]; |
| let factors = quantizer.encode(&residual, fvec_norm_l2sqr(&vector), &mut code); |
| let dist = quantizer.distance_to_code(&residual, &vector, &code, factors, MetricType::L2); |
| |
| assert!( |
| dist <= 1e-5, |
| "self distance should be close to zero: {dist}" |
| ); |
| } |
| |
| #[test] |
| fn distance_context_byte_lut_matches_scalar_path() { |
| let d = 16; |
| let quantizer = RaBitQuantizer::new(d); |
| let rotated_residual: Vec<f32> = (0..d).map(|i| i as f32 * 0.25 - 1.5).collect(); |
| let query: Vec<f32> = (0..d).map(|i| (i as f32 + 1.0) * 0.125).collect(); |
| let rotated_query_residual: Vec<f32> = (0..d).map(|i| (i as f32 - 3.0) * 0.2).collect(); |
| |
| let mut code = vec![0u8; quantizer.code_size()]; |
| let factors = quantizer.encode(&rotated_residual, fvec_norm_l2sqr(&query), &mut code); |
| code[0] = 0b1010_0101; |
| code[1] = 0b0101_1010; |
| |
| let scalar_context = |
| quantizer.prepare_distance_context(rotated_query_residual.clone(), &query, false); |
| let lut_context = quantizer.prepare_distance_context(rotated_query_residual, &query, true); |
| |
| for metric in [MetricType::L2, MetricType::Cosine, MetricType::InnerProduct] { |
| let scalar = |
| quantizer.distance_to_code_prepared(&scalar_context, &code, factors, metric); |
| let lut = quantizer.distance_to_code_prepared(&lut_context, &code, factors, metric); |
| assert!( |
| (scalar - lut).abs() < 1e-5, |
| "metric {:?}: scalar {} != lut {}", |
| metric, |
| scalar, |
| lut |
| ); |
| } |
| } |
| |
| #[test] |
| fn byte_lut_matches_scalar_signed_sum_for_every_pattern() { |
| let d = 13; |
| let quantizer = RaBitQuantizer::new(d); |
| let residual: Vec<f32> = (0..d).map(|i| i as f32 * 0.37 - 2.1).collect(); |
| let lut = quantizer.build_byte_signed_sums(&residual); |
| let mut code = vec![0u8; quantizer.code_size()]; |
| |
| for first_byte in 0..=u8::MAX { |
| for second_byte in 0..=u8::MAX { |
| code[0] = first_byte; |
| code[1] = second_byte; |
| let scalar: f32 = residual |
| .iter() |
| .enumerate() |
| .map(|(dim, &value)| if get_bit(&code, dim) { value } else { -value }) |
| .sum(); |
| let actual = lut[first_byte as usize] + lut[256 + second_byte as usize]; |
| assert!( |
| (actual - scalar).abs() < 1e-5, |
| "code {first_byte:#010b} {second_byte:#010b}: {actual} != {scalar}" |
| ); |
| } |
| } |
| } |
| |
| #[test] |
| fn quantized_query_bit_planes_match_scalar_quantization() { |
| let d = 24; |
| let quantizer = RaBitQuantizer::new(d); |
| let rotated_query_residual: Vec<f32> = (0..d).map(|i| (i as f32 - 11.0) * 0.17).collect(); |
| let query: Vec<f32> = (0..d).map(|i| (i as f32 + 1.0) * 0.03125).collect(); |
| let mut code = vec![0u8; quantizer.code_size()]; |
| for (byte_idx, byte) in code.iter_mut().enumerate() { |
| *byte = if byte_idx % 2 == 0 { |
| 0b1010_1100 |
| } else { |
| 0b0101_0011 |
| }; |
| } |
| |
| for query_bits in [4, 8] { |
| let context = quantizer.prepare_distance_context_with_query_bits( |
| rotated_query_residual.clone(), |
| &query, |
| true, |
| query_bits, |
| ); |
| let quantized_query = context.quantized_query.as_ref().unwrap(); |
| let actual = quantized_query.signed_query_sum(&code, quantizer.code_size()); |
| let expected = |
| scalar_quantized_signed_query_sum(&rotated_query_residual, &code, query_bits); |
| |
| assert!( |
| (actual - expected).abs() < 1e-5, |
| "query_bits {}: {} != {}", |
| query_bits, |
| actual, |
| expected |
| ); |
| assert!(context.byte_signed_sums.is_none()); |
| } |
| } |
| |
| #[test] |
| fn norm_l2sqr_abs_sum_helper_matches_expected_values() { |
| let values = [-3.0f32, 4.0, 0.5, -0.25, 8.0, -2.0, 1.25, -6.0, 7.0]; |
| let (norm, abs_sum) = fvec_norm_l2sqr_abs_sum(&values); |
| |
| let expected_norm: f32 = values.iter().map(|value| value * value).sum(); |
| let expected_abs_sum: f32 = values.iter().map(|value| value.abs()).sum(); |
| assert!((norm - expected_norm).abs() < 1e-5); |
| assert!((abs_sum - expected_abs_sum).abs() < 1e-5); |
| } |
| |
| #[test] |
| fn rotation_preserves_l2_norm() { |
| let d = 16; |
| let rotation = RQRotation::new(d, 11, 3); |
| let mut vector: Vec<f32> = (0..d).map(|i| i as f32 - 3.0).collect(); |
| let before = fvec_norm_l2sqr(&vector); |
| rotation.apply(&mut vector); |
| let after = fvec_norm_l2sqr(&vector); |
| assert!((before - after).abs() < 1e-4); |
| } |
| |
| fn scalar_quantized_signed_query_sum( |
| rotated_query_residual: &[f32], |
| code: &[u8], |
| query_bits: usize, |
| ) -> f32 { |
| let magnitude_bits = query_bits - 1; |
| let max_level = (1usize << magnitude_bits) - 1; |
| let max_abs = rotated_query_residual |
| .iter() |
| .map(|value| value.abs()) |
| .fold(0.0f32, f32::max); |
| if max_abs <= f32::EPSILON { |
| return 0.0; |
| } |
| let scale = max_abs / max_level as f32; |
| let mut sum = 0i64; |
| for (dim, &value) in rotated_query_residual.iter().enumerate() { |
| let code_sign = if get_bit(code, dim) { 1i64 } else { -1i64 }; |
| let query_sign = if value >= 0.0 { 1i64 } else { -1i64 }; |
| let level = (value.abs() / scale).round().clamp(0.0, max_level as f32) as i64; |
| sum += code_sign * query_sign * level; |
| } |
| scale * sum as f32 |
| } |
| } |