blob: 112f887c008db53e06a02db8f07ed35339cc22ea [file]
// 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};
#[derive(Debug, Clone, PartialEq)]
pub struct ScalarQuantizer {
pub d: usize,
pub min: f32,
pub max: f32,
pub mins: Vec<f32>,
pub maxs: Vec<f32>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ScalarQuantizerDecodeLut {
d: usize,
values: Vec<f32>,
}
impl ScalarQuantizer {
pub fn new(d: usize) -> Self {
Self {
d,
min: 0.0,
max: 0.0,
mins: vec![0.0; d],
maxs: vec![0.0; d],
}
}
pub fn with_bounds(d: usize, min: f32, max: f32) -> Self {
Self {
d,
min,
max,
mins: vec![min; d],
maxs: vec![max; d],
}
}
pub fn with_dimension_bounds(d: usize, mins: Vec<f32>, maxs: Vec<f32>) -> Self {
assert_eq!(mins.len(), d);
assert_eq!(maxs.len(), d);
let mut sq = Self {
d,
min: 0.0,
max: 0.0,
mins,
maxs,
};
sq.refresh_global_bounds();
sq
}
pub fn train(&mut self, data: &[f32], n: usize) {
let len = n * self.d;
let values = &data[..len];
if n == 0 || self.d == 0 {
self.min = 0.0;
self.max = 0.0;
self.mins.fill(0.0);
self.maxs.fill(0.0);
return;
}
self.ensure_bounds_len();
self.mins.fill(f32::INFINITY);
self.maxs.fill(f32::NEG_INFINITY);
update_bounds_batch(values, n, self.d, &mut self.mins, &mut self.maxs);
self.refresh_global_bounds();
}
pub fn code_size(&self) -> usize {
self.d
}
pub fn encode_batch(&self, data: &[f32], n: usize, codes: &mut [u8]) {
let len = n * self.d;
assert!(data.len() >= len);
assert!(codes.len() >= len);
encode_batch_simd(data, n, self.d, &self.mins, &self.maxs, codes);
}
pub fn encode(&self, vector: &[f32], code: &mut [u8]) {
self.encode_batch(vector, 1, code);
}
pub fn decode_batch(&self, codes: &[u8], n: usize, vectors: &mut [f32]) {
let len = n * self.d;
assert!(codes.len() >= len);
assert!(vectors.len() >= len);
for row in 0..n {
let base = row * self.d;
for dim in 0..self.d {
vectors[base + dim] = self.decode_value(codes[base + dim], dim);
}
}
}
pub fn decode_batch_with_offset(
&self,
codes: &[u8],
n: usize,
offset: &[f32],
vectors: &mut [f32],
) {
let len = n * self.d;
assert!(codes.len() >= len);
assert!(offset.len() >= self.d);
assert!(vectors.len() >= len);
decode_batch_with_offset_simd(codes, n, self.d, &self.mins, &self.maxs, offset, vectors);
}
pub fn decode(&self, code: &[u8], vector: &mut [f32]) {
self.decode_batch(code, 1, vector);
}
pub fn distance_to_code(&self, query: &[f32], code: &[u8], metric: MetricType) -> f32 {
self.distance_to_code_with_context(query, code, self.distance_context(query, metric))
}
pub fn distance_context(&self, query: &[f32], metric: MetricType) -> DistanceContext {
debug_assert!(query.len() >= self.d);
DistanceContext::new(&query[..self.d], metric)
}
pub fn distance_to_code_with_context(
&self,
query: &[f32],
code: &[u8],
context: DistanceContext,
) -> f32 {
self.distance_to_code_impl(query, code, &[], false, context)
}
pub fn distance_to_code_with_offset_with_context(
&self,
query: &[f32],
code: &[u8],
offset: &[f32],
context: DistanceContext,
) -> f32 {
debug_assert!(query.len() >= self.d);
debug_assert!(code.len() >= self.d);
debug_assert!(offset.len() >= self.d);
self.distance_to_code_impl(query, code, offset, true, context)
}
pub fn distance_to_code_with_lut_with_context(
&self,
query: &[f32],
code: &[u8],
lut: &ScalarQuantizerDecodeLut,
context: DistanceContext,
) -> f32 {
self.distance_to_code_lut_impl(query, code, &[], false, lut, context)
}
pub fn distance_to_code_with_lut_offset_with_context(
&self,
query: &[f32],
code: &[u8],
offset: &[f32],
lut: &ScalarQuantizerDecodeLut,
context: DistanceContext,
) -> f32 {
debug_assert!(query.len() >= self.d);
debug_assert!(code.len() >= self.d);
debug_assert!(offset.len() >= self.d);
self.distance_to_code_lut_impl(query, code, offset, true, lut, context)
}
pub fn build_decode_lut(&self) -> ScalarQuantizerDecodeLut {
let mut values = vec![0.0f32; self.d * 256];
for dim in 0..self.d {
let base = dim * 256;
for code in 0..256 {
values[base + code] = self.decode_value(code as u8, dim);
}
}
ScalarQuantizerDecodeLut { d: self.d, values }
}
fn distance_to_code_impl(
&self,
query: &[f32],
code: &[u8],
offset: &[f32],
use_offset: bool,
context: DistanceContext,
) -> f32 {
debug_assert!(query.len() >= self.d);
debug_assert!(code.len() >= self.d);
match context.metric {
MetricType::L2 => {
let mut sum = 0.0f32;
for i in 0..self.d {
let diff =
query[i] - self.decode_value_with_offset(code[i], i, offset, use_offset);
sum += diff * diff;
}
sum
}
MetricType::InnerProduct => {
let mut dot = 0.0f32;
for i in 0..self.d {
dot += query[i] * self.decode_value_with_offset(code[i], i, offset, use_offset);
}
-dot
}
MetricType::Cosine => {
let mut dot = 0.0f32;
let mut vector_norm = 0.0f32;
for i in 0..self.d {
let value = self.decode_value_with_offset(code[i], i, offset, use_offset);
dot += query[i] * value;
vector_norm += value * value;
}
let denom = context.query_norm * vector_norm.sqrt();
if denom > 0.0 {
1.0 - dot / denom
} else {
1.0
}
}
}
}
fn distance_to_code_lut_impl(
&self,
query: &[f32],
code: &[u8],
offset: &[f32],
use_offset: bool,
lut: &ScalarQuantizerDecodeLut,
context: DistanceContext,
) -> f32 {
debug_assert!(query.len() >= self.d);
debug_assert!(code.len() >= self.d);
debug_assert_eq!(lut.d, self.d);
match context.metric {
MetricType::L2 => {
let mut sum = 0.0f32;
for i in 0..self.d {
let diff = query[i]
- decoded_lut_value_with_offset(lut, code[i], i, offset, use_offset);
sum += diff * diff;
}
sum
}
MetricType::InnerProduct => {
let mut dot = 0.0f32;
for i in 0..self.d {
dot += query[i]
* decoded_lut_value_with_offset(lut, code[i], i, offset, use_offset);
}
-dot
}
MetricType::Cosine => {
let mut dot = 0.0f32;
let mut vector_norm = 0.0f32;
for i in 0..self.d {
let value = decoded_lut_value_with_offset(lut, code[i], i, offset, use_offset);
dot += query[i] * value;
vector_norm += value * value;
}
let denom = context.query_norm * vector_norm.sqrt();
if denom > 0.0 {
1.0 - dot / denom
} else {
1.0
}
}
}
}
fn decode_value_with_offset(
&self,
code: u8,
dim: usize,
offset: &[f32],
use_offset: bool,
) -> f32 {
let value = self.decode_value(code, dim);
if use_offset {
value + offset[dim]
} else {
value
}
}
fn decode_value(&self, code: u8, dim: usize) -> f32 {
let min = self.mins[dim];
let max = self.maxs[dim];
if min >= max {
min
} else {
min + code as f32 * (max - min) / 255.0
}
}
fn ensure_bounds_len(&mut self) {
if self.mins.len() != self.d {
self.mins.resize(self.d, 0.0);
}
if self.maxs.len() != self.d {
self.maxs.resize(self.d, 0.0);
}
}
fn refresh_global_bounds(&mut self) {
if self.d == 0 {
self.min = 0.0;
self.max = 0.0;
return;
}
self.min = self.mins.iter().copied().fold(f32::INFINITY, f32::min);
self.max = self.maxs.iter().copied().fold(f32::NEG_INFINITY, f32::max);
}
}
fn update_bounds_batch(data: &[f32], n: usize, d: usize, mins: &mut [f32], maxs: &mut [f32]) {
update_bounds_batch_simd(data, n, d, mins, maxs);
}
#[cfg(target_arch = "x86_64")]
#[inline]
fn update_bounds_batch_simd(data: &[f32], n: usize, d: usize, mins: &mut [f32], maxs: &mut [f32]) {
if is_x86_feature_detected!("avx2") && d >= 8 {
unsafe { update_bounds_batch_avx2(data, n, d, mins, maxs) };
} else {
update_bounds_batch_scalar(data, n, d, mins, maxs);
}
}
#[cfg(target_arch = "aarch64")]
#[inline]
fn update_bounds_batch_simd(data: &[f32], n: usize, d: usize, mins: &mut [f32], maxs: &mut [f32]) {
unsafe { update_bounds_batch_neon(data, n, d, mins, maxs) };
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
#[inline]
fn update_bounds_batch_simd(data: &[f32], n: usize, d: usize, mins: &mut [f32], maxs: &mut [f32]) {
update_bounds_batch_scalar(data, n, d, mins, maxs);
}
#[cfg(not(target_arch = "aarch64"))]
fn update_bounds_batch_scalar(
data: &[f32],
n: usize,
d: usize,
mins: &mut [f32],
maxs: &mut [f32],
) {
for vector in data[..n * d].chunks_exact(d) {
for i in 0..d {
mins[i] = mins[i].min(vector[i]);
maxs[i] = maxs[i].max(vector[i]);
}
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn update_bounds_batch_avx2(
data: &[f32],
n: usize,
d: usize,
mins: &mut [f32],
maxs: &mut [f32],
) {
use std::arch::x86_64::*;
for row in 0..n {
let base = row * d;
let mut dim = 0;
while dim + 8 <= d {
let values = unsafe { _mm256_loadu_ps(data.as_ptr().add(base + dim)) };
let current_min = unsafe { _mm256_loadu_ps(mins.as_ptr().add(dim)) };
let current_max = unsafe { _mm256_loadu_ps(maxs.as_ptr().add(dim)) };
unsafe {
_mm256_storeu_ps(
mins.as_mut_ptr().add(dim),
_mm256_min_ps(current_min, values),
);
_mm256_storeu_ps(
maxs.as_mut_ptr().add(dim),
_mm256_max_ps(current_max, values),
);
}
dim += 8;
}
while dim < d {
let value = unsafe { *data.get_unchecked(base + dim) };
let min_ref = unsafe { mins.get_unchecked_mut(dim) };
*min_ref = min_ref.min(value);
let max_ref = unsafe { maxs.get_unchecked_mut(dim) };
*max_ref = max_ref.max(value);
dim += 1;
}
}
}
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn update_bounds_batch_neon(
data: &[f32],
n: usize,
d: usize,
mins: &mut [f32],
maxs: &mut [f32],
) {
use std::arch::aarch64::*;
for row in 0..n {
let base = row * d;
let mut dim = 0;
while dim + 4 <= d {
let values = unsafe { vld1q_f32(data.as_ptr().add(base + dim)) };
let current_min = unsafe { vld1q_f32(mins.as_ptr().add(dim)) };
let current_max = unsafe { vld1q_f32(maxs.as_ptr().add(dim)) };
unsafe {
vst1q_f32(mins.as_mut_ptr().add(dim), vminq_f32(current_min, values));
vst1q_f32(maxs.as_mut_ptr().add(dim), vmaxq_f32(current_max, values));
}
dim += 4;
}
while dim < d {
let value = unsafe { *data.get_unchecked(base + dim) };
let min_ref = unsafe { mins.get_unchecked_mut(dim) };
*min_ref = min_ref.min(value);
let max_ref = unsafe { maxs.get_unchecked_mut(dim) };
*max_ref = max_ref.max(value);
dim += 1;
}
}
}
fn encode_batch_simd(
data: &[f32],
n: usize,
d: usize,
mins: &[f32],
maxs: &[f32],
codes: &mut [u8],
) {
encode_batch_simd_impl(data, n, d, mins, maxs, codes);
}
#[cfg(target_arch = "x86_64")]
#[inline]
fn encode_batch_simd_impl(
data: &[f32],
n: usize,
d: usize,
mins: &[f32],
maxs: &[f32],
codes: &mut [u8],
) {
if is_x86_feature_detected!("avx2") && d >= 8 {
unsafe { encode_batch_avx2(data, n, d, mins, maxs, codes) };
} else {
encode_batch_scalar(data, n, d, mins, maxs, codes);
}
}
#[cfg(target_arch = "aarch64")]
#[inline]
fn encode_batch_simd_impl(
data: &[f32],
n: usize,
d: usize,
mins: &[f32],
maxs: &[f32],
codes: &mut [u8],
) {
unsafe { encode_batch_neon(data, n, d, mins, maxs, codes) };
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
#[inline]
fn encode_batch_simd_impl(
data: &[f32],
n: usize,
d: usize,
mins: &[f32],
maxs: &[f32],
codes: &mut [u8],
) {
encode_batch_scalar(data, n, d, mins, maxs, codes);
}
#[cfg(not(target_arch = "aarch64"))]
fn encode_batch_scalar(
data: &[f32],
n: usize,
d: usize,
mins: &[f32],
maxs: &[f32],
codes: &mut [u8],
) {
for row in 0..n {
let base = row * d;
for dim in 0..d {
codes[base + dim] = encode_value(data[base + dim], mins[dim], maxs[dim]);
}
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn encode_batch_avx2(
data: &[f32],
n: usize,
d: usize,
mins: &[f32],
maxs: &[f32],
codes: &mut [u8],
) {
use std::arch::x86_64::*;
let zero = _mm256_setzero_ps();
let one = _mm256_set1_ps(1.0);
let max_code = _mm256_set1_ps(255.0);
let mut scaled = [0.0f32; 8];
for row in 0..n {
let base = row * d;
let mut dim = 0;
while dim + 8 <= d {
let values = unsafe { _mm256_loadu_ps(data.as_ptr().add(base + dim)) };
let minv = unsafe { _mm256_loadu_ps(mins.as_ptr().add(dim)) };
let maxv = unsafe { _mm256_loadu_ps(maxs.as_ptr().add(dim)) };
let range = _mm256_sub_ps(maxv, minv);
let valid = _mm256_cmp_ps::<_CMP_GT_OQ>(maxv, minv);
let safe_range = _mm256_blendv_ps(one, range, valid);
let scale = _mm256_blendv_ps(zero, _mm256_div_ps(max_code, safe_range), valid);
let encoded = _mm256_min_ps(
max_code,
_mm256_max_ps(zero, _mm256_mul_ps(_mm256_sub_ps(values, minv), scale)),
);
unsafe { _mm256_storeu_ps(scaled.as_mut_ptr(), encoded) };
for lane in 0..8 {
codes[base + dim + lane] = scaled[lane].round() as u8;
}
dim += 8;
}
while dim < d {
codes[base + dim] = encode_value(data[base + dim], mins[dim], maxs[dim]);
dim += 1;
}
}
}
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn encode_batch_neon(
data: &[f32],
n: usize,
d: usize,
mins: &[f32],
maxs: &[f32],
codes: &mut [u8],
) {
use std::arch::aarch64::*;
let zero = vdupq_n_f32(0.0);
let one = vdupq_n_f32(1.0);
let max_code = vdupq_n_f32(255.0);
let mut scaled = [0.0f32; 4];
for row in 0..n {
let base = row * d;
let mut dim = 0;
while dim + 4 <= d {
let values = unsafe { vld1q_f32(data.as_ptr().add(base + dim)) };
let minv = unsafe { vld1q_f32(mins.as_ptr().add(dim)) };
let maxv = unsafe { vld1q_f32(maxs.as_ptr().add(dim)) };
let range = vsubq_f32(maxv, minv);
let valid = vcgtq_f32(maxv, minv);
let safe_range = vbslq_f32(valid, range, one);
let scale = vbslq_f32(valid, vdivq_f32(max_code, safe_range), zero);
let encoded = vminq_f32(
max_code,
vmaxq_f32(zero, vmulq_f32(vsubq_f32(values, minv), scale)),
);
unsafe { vst1q_f32(scaled.as_mut_ptr(), encoded) };
for lane in 0..4 {
codes[base + dim + lane] = scaled[lane].round() as u8;
}
dim += 4;
}
while dim < d {
codes[base + dim] = encode_value(data[base + dim], mins[dim], maxs[dim]);
dim += 1;
}
}
}
#[inline]
fn encode_value(value: f32, min: f32, max: f32) -> u8 {
if min >= max {
0
} else {
((value - min) * 255.0 / (max - min))
.clamp(0.0, 255.0)
.round() as u8
}
}
fn decode_batch_with_offset_simd(
codes: &[u8],
n: usize,
d: usize,
mins: &[f32],
maxs: &[f32],
offset: &[f32],
vectors: &mut [f32],
) {
decode_batch_with_offset_simd_impl(codes, n, d, mins, maxs, offset, vectors);
}
#[cfg(target_arch = "x86_64")]
#[inline]
fn decode_batch_with_offset_simd_impl(
codes: &[u8],
n: usize,
d: usize,
mins: &[f32],
maxs: &[f32],
offset: &[f32],
vectors: &mut [f32],
) {
if is_x86_feature_detected!("avx2") && d >= 8 {
unsafe { decode_batch_with_offset_avx2(codes, n, d, mins, maxs, offset, vectors) };
} else {
decode_batch_with_offset_scalar(codes, n, d, mins, maxs, offset, vectors);
}
}
#[cfg(target_arch = "aarch64")]
#[inline]
fn decode_batch_with_offset_simd_impl(
codes: &[u8],
n: usize,
d: usize,
mins: &[f32],
maxs: &[f32],
offset: &[f32],
vectors: &mut [f32],
) {
unsafe { decode_batch_with_offset_neon(codes, n, d, mins, maxs, offset, vectors) };
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
#[inline]
fn decode_batch_with_offset_simd_impl(
codes: &[u8],
n: usize,
d: usize,
mins: &[f32],
maxs: &[f32],
offset: &[f32],
vectors: &mut [f32],
) {
decode_batch_with_offset_scalar(codes, n, d, mins, maxs, offset, vectors);
}
#[cfg(not(target_arch = "aarch64"))]
fn decode_batch_with_offset_scalar(
codes: &[u8],
n: usize,
d: usize,
mins: &[f32],
maxs: &[f32],
offset: &[f32],
vectors: &mut [f32],
) {
for row in 0..n {
let base = row * d;
for dim in 0..d {
vectors[base + dim] =
decode_value(codes[base + dim], mins[dim], maxs[dim]) + offset[dim];
}
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn decode_batch_with_offset_avx2(
codes: &[u8],
n: usize,
d: usize,
mins: &[f32],
maxs: &[f32],
offset: &[f32],
vectors: &mut [f32],
) {
use std::arch::x86_64::*;
let inv_255 = _mm256_set1_ps(1.0 / 255.0);
for row in 0..n {
let base = row * d;
let mut dim = 0;
while dim + 8 <= d {
let code_bytes = unsafe { _mm_loadl_epi64(codes.as_ptr().add(base + dim).cast()) };
let code_i32 = _mm256_cvtepu8_epi32(code_bytes);
let code_f32 = _mm256_cvtepi32_ps(code_i32);
let minv = unsafe { _mm256_loadu_ps(mins.as_ptr().add(dim)) };
let maxv = unsafe { _mm256_loadu_ps(maxs.as_ptr().add(dim)) };
let offsetv = unsafe { _mm256_loadu_ps(offset.as_ptr().add(dim)) };
let decoded = _mm256_add_ps(
offsetv,
_mm256_add_ps(
minv,
_mm256_mul_ps(code_f32, _mm256_mul_ps(_mm256_sub_ps(maxv, minv), inv_255)),
),
);
let constant = _mm256_cmp_ps::<_CMP_GE_OQ>(minv, maxv);
let constant_decoded = _mm256_add_ps(minv, offsetv);
let result = _mm256_blendv_ps(decoded, constant_decoded, constant);
unsafe { _mm256_storeu_ps(vectors.as_mut_ptr().add(base + dim), result) };
dim += 8;
}
while dim < d {
vectors[base + dim] =
decode_value(codes[base + dim], mins[dim], maxs[dim]) + offset[dim];
dim += 1;
}
}
}
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn decode_batch_with_offset_neon(
codes: &[u8],
n: usize,
d: usize,
mins: &[f32],
maxs: &[f32],
offset: &[f32],
vectors: &mut [f32],
) {
use std::arch::aarch64::*;
let inv_255 = vdupq_n_f32(1.0 / 255.0);
for row in 0..n {
let base = row * d;
let mut dim = 0;
while dim + 8 <= d {
let code_u8 = unsafe { vld1_u8(codes.as_ptr().add(base + dim)) };
let code_u16 = vmovl_u8(code_u8);
let low_u32 = vmovl_u16(vget_low_u16(code_u16));
let high_u32 = vmovl_u16(vget_high_u16(code_u16));
let min0 = unsafe { vld1q_f32(mins.as_ptr().add(dim)) };
let max0 = unsafe { vld1q_f32(maxs.as_ptr().add(dim)) };
let offset0 = unsafe { vld1q_f32(offset.as_ptr().add(dim)) };
let decoded0 = vaddq_f32(
offset0,
vaddq_f32(
min0,
vmulq_f32(
vcvtq_f32_u32(low_u32),
vmulq_f32(vsubq_f32(max0, min0), inv_255),
),
),
);
let constant0 = vcgeq_f32(min0, max0);
let result0 = vbslq_f32(constant0, vaddq_f32(min0, offset0), decoded0);
unsafe { vst1q_f32(vectors.as_mut_ptr().add(base + dim), result0) };
let min1 = unsafe { vld1q_f32(mins.as_ptr().add(dim + 4)) };
let max1 = unsafe { vld1q_f32(maxs.as_ptr().add(dim + 4)) };
let offset1 = unsafe { vld1q_f32(offset.as_ptr().add(dim + 4)) };
let decoded1 = vaddq_f32(
offset1,
vaddq_f32(
min1,
vmulq_f32(
vcvtq_f32_u32(high_u32),
vmulq_f32(vsubq_f32(max1, min1), inv_255),
),
),
);
let constant1 = vcgeq_f32(min1, max1);
let result1 = vbslq_f32(constant1, vaddq_f32(min1, offset1), decoded1);
unsafe { vst1q_f32(vectors.as_mut_ptr().add(base + dim + 4), result1) };
dim += 8;
}
while dim < d {
vectors[base + dim] =
decode_value(codes[base + dim], mins[dim], maxs[dim]) + offset[dim];
dim += 1;
}
}
}
#[inline]
fn decode_value(code: u8, min: f32, max: f32) -> f32 {
if min >= max {
min
} else {
min + code as f32 * (max - min) / 255.0
}
}
impl ScalarQuantizerDecodeLut {
#[inline]
pub fn decode_value(&self, code: u8, dim: usize) -> f32 {
debug_assert!(dim < self.d);
self.values[dim * 256 + code as usize]
}
pub fn dimension(&self) -> usize {
self.d
}
}
#[inline]
fn decoded_lut_value_with_offset(
lut: &ScalarQuantizerDecodeLut,
code: u8,
dim: usize,
offset: &[f32],
use_offset: bool,
) -> f32 {
let value = lut.decode_value(code, dim);
if use_offset {
value + offset[dim]
} else {
value
}
}
#[derive(Debug, Clone, Copy)]
pub struct DistanceContext {
metric: MetricType,
query_norm: f32,
}
impl DistanceContext {
pub fn new(query: &[f32], metric: MetricType) -> Self {
let query_norm = if metric == MetricType::Cosine {
fvec_norm_l2sqr(query).sqrt()
} else {
0.0
};
Self { metric, query_norm }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scalar_quantizer_round_trips_bounds() {
let data = vec![-1.0, 0.0, 1.0, 3.0];
let mut sq = ScalarQuantizer::new(2);
sq.train(&data, 2);
let mut codes = vec![0u8; data.len()];
sq.encode_batch(&data, 2, &mut codes);
let mut decoded = vec![0.0f32; data.len()];
sq.decode_batch(&codes, 2, &mut decoded);
assert_eq!(sq.min, -1.0);
assert_eq!(sq.max, 3.0);
assert_eq!(sq.mins, vec![-1.0, 0.0]);
assert_eq!(sq.maxs, vec![1.0, 3.0]);
assert_eq!(codes[0], 0);
assert_eq!(codes[3], 255);
assert!((decoded[0] + 1.0).abs() < 1e-6);
assert!((decoded[3] - 3.0).abs() < 1e-6);
}
#[test]
fn test_scalar_quantizer_constant_input() {
let data = vec![5.0, 5.0, 5.0, 5.0];
let mut sq = ScalarQuantizer::new(2);
sq.train(&data, 2);
let mut codes = vec![7u8; data.len()];
sq.encode_batch(&data, 2, &mut codes);
let mut decoded = vec![0.0f32; data.len()];
sq.decode_batch(&codes, 2, &mut decoded);
assert_eq!(codes, vec![0, 0, 0, 0]);
assert_eq!(decoded, data);
}
#[test]
fn test_scalar_quantizer_uses_per_dimension_bounds() {
let data = vec![0.0, -100.0, 1.0, 100.0];
let mut sq = ScalarQuantizer::new(2);
sq.train(&data, 2);
let mut codes = vec![0u8; data.len()];
sq.encode_batch(&data, 2, &mut codes);
let mut decoded = vec![0.0f32; data.len()];
sq.decode_batch(&codes, 2, &mut decoded);
assert_eq!(codes, vec![0, 0, 255, 255]);
assert!((decoded[0] - 0.0).abs() < 1e-6);
assert!((decoded[1] + 100.0).abs() < 1e-6);
assert!((decoded[2] - 1.0).abs() < 1e-6);
assert!((decoded[3] - 100.0).abs() < 1e-6);
}
#[test]
fn test_scalar_quantizer_wide_batch_paths() {
let d = 9;
let n = 5;
let data: Vec<f32> = (0..n * d)
.map(|i| ((i * 7 % 23) as f32) * 0.5 - 3.0)
.collect();
let mut sq = ScalarQuantizer::new(d);
sq.train(&data, n);
for dim in 0..d {
let expected_min = (0..n)
.map(|row| data[row * d + dim])
.fold(f32::INFINITY, f32::min);
let expected_max = (0..n)
.map(|row| data[row * d + dim])
.fold(f32::NEG_INFINITY, f32::max);
assert_eq!(sq.mins[dim], expected_min);
assert_eq!(sq.maxs[dim], expected_max);
}
let mut codes = vec![0u8; n * d];
sq.encode_batch(&data, n, &mut codes);
let mut decoded = vec![0.0f32; n * d];
let offset: Vec<f32> = (0..d).map(|dim| dim as f32 * 0.25).collect();
sq.decode_batch_with_offset(&codes, n, &offset, &mut decoded);
for row in 0..n {
for dim in 0..d {
let expected = sq.decode_value(codes[row * d + dim], dim) + offset[dim];
assert!((decoded[row * d + dim] - expected).abs() < 1e-6);
}
}
}
#[test]
fn test_scalar_quantizer_distance_to_code() {
let sq = ScalarQuantizer::with_bounds(2, 0.0, 1.0);
let mut code = vec![0u8; 2];
sq.encode(&[1.0, 0.0], &mut code);
let dist = sq.distance_to_code(&[1.0, 0.0], &code, MetricType::L2);
assert!(dist < 1e-6);
}
#[test]
fn test_scalar_quantizer_decode_batch_with_offset() {
let sq = ScalarQuantizer::with_dimension_bounds(2, vec![0.0, -1.0], vec![1.0, 1.0]);
let codes = vec![255, 0, 0, 255];
let mut decoded = vec![0.0f32; 4];
sq.decode_batch_with_offset(&codes, 2, &[10.0, 20.0], &mut decoded);
assert!((decoded[0] - 11.0).abs() < 1e-6);
assert!((decoded[1] - 19.0).abs() < 1e-6);
assert!((decoded[2] - 10.0).abs() < 1e-6);
assert!((decoded[3] - 21.0).abs() < 1e-6);
}
}