pub(crate) mod encoding;
pub mod lloyd_max;
mod math;
mod permutation;
pub mod quantization;
pub mod rotation;
pub mod simd;
use std::alloc::Layout;
use std::borrow::Cow;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use crate::common::counter::hardware_counter::HardwareCounterCell;
use crate::common::fs::atomic_save_json;
use crate::common::mmap::MmapFlusher;
use crate::common::typelevel::True;
use crate::common::types::PointOffsetType;
use fs_err as fs;
use serde::{Deserialize, Serialize};
use crate::quantization::EncodingError;
use crate::quantization::encoded_storage::{EncodedStorage, EncodedStorageBuilder};
use crate::quantization::encoded_vectors::{EncodedVectors, VectorParameters, validate_vector_parameters};
use crate::quantization::quantile::find_quantile_interval_per_coordinate_with_preprocess;
use crate::quantization::turboquant::math::std_normal_cdf;
use crate::quantization::turboquant::quantization::{ErrorCorrection, TurboQuantizer};
use crate::quantization::turboquant::simd::{Query1bitSimd, Query2bitSimd, Query4bitSimd};
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum TQBits {
Bits4,
Bits2,
Bits1_5,
Bits1,
}
impl TQBits {
#[inline]
fn bit_size(&self) -> u8 {
match self {
TQBits::Bits4 => 4,
TQBits::Bits2 => 2,
TQBits::Bits1_5 => 1,
TQBits::Bits1 => 1,
}
}
#[inline]
pub(crate) fn sample_size(&self) -> usize {
match self {
TQBits::Bits1 | TQBits::Bits1_5 => 2_048,
TQBits::Bits2 => 4_096,
TQBits::Bits4 => 8_192,
}
}
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum TQMode {
Normal,
Plus,
}
pub struct EncodedVectorsTQ<TStorage: EncodedStorage> {
encoded_vectors: TStorage,
metadata: Metadata,
metadata_path: Option<PathBuf>,
quantizer: TurboQuantizer,
encoding_buffer: Vec<f64>,
}
pub struct EncodedQueryTQ {
data: EncodedQueryTQData,
l2_norm: Option<f32>,
query: Option<Vec<f32>>,
ec_correction: f32,
}
pub enum EncodedQueryTQData {
Bits1(Query1bitSimd),
Bits1Wide(Query1bitSimd<16>),
Bits2(Query2bitSimd),
Bits4(Query4bitSimd),
}
#[derive(Serialize, Deserialize)]
pub struct Metadata {
pub vector_parameters: VectorParameters,
pub bits: TQBits,
pub mode: TQMode,
pub error_correction: Option<ErrorCorrectionMetadata>,
}
#[derive(Serialize, Deserialize)]
pub struct ErrorCorrectionMetadata {
pub shift: Vec<f32>,
pub scale: Vec<f32>,
}
impl<TStorage: EncodedStorage> EncodedVectorsTQ<TStorage> {
pub fn storage(&self) -> &TStorage {
&self.encoded_vectors
}
#[allow(clippy::too_many_arguments)]
pub fn encode<'a>(
data: impl Iterator<Item = impl AsRef<[f32]> + 'a> + Clone + 'a,
mut storage_builder: impl EncodedStorageBuilder<Storage = TStorage>,
vector_parameters: &VectorParameters,
count: usize,
bits: TQBits,
mode: TQMode,
num_threads: usize,
meta_path: Option<&Path>,
stopped: &AtomicBool,
) -> Result<Self, EncodingError> {
debug_assert!(validate_vector_parameters(data.clone(), vector_parameters).is_ok());
let error_correction = match mode {
TQMode::Normal => None,
TQMode::Plus => {
let pre_quantizer = TurboQuantizer::new_from_metadata(&Metadata {
vector_parameters: *vector_parameters,
bits,
mode,
error_correction: None,
})
.map_err(|e| {
EncodingError::EncodingError(format!(
"Failed to construct pre-quantizer for TQ+ stats pass: {e}",
))
})?;
let padded_dim = pre_quantizer.padded_dim;
let centroids = bits.get_centroids();
let c_outer = centroids
.iter()
.copied()
.fold(0.0_f32, |acc, c| acc.max(c.abs()));
let p_outer = std_normal_cdf(f64::from(c_outer));
let quantile_param = ((2.0 * p_outer - 1.0) as f32).clamp(0.0, 0.999_99);
let pre_quantizer_ref = &pre_quantizer;
let intervals = find_quantile_interval_per_coordinate_with_preprocess(
data.clone(),
vector_parameters.dim,
padded_dim,
count,
quantile_param,
num_threads,
bits.sample_size(),
move |raw, scratch| {
pre_quantizer_ref.preprocess_into(raw, scratch);
},
stopped,
)?;
const MIN_QUANTILE_WIDTH: f32 = 1e-3;
let mut shift = vec![0.0f32; padded_dim];
let mut scale = vec![1.0f32; padded_dim];
for (i, &(q_lo, q_hi)) in intervals.iter().enumerate() {
shift[i] = -(q_lo + q_hi) / 2.0;
let denom = q_hi - q_lo;
if denom > MIN_QUANTILE_WIDTH {
scale[i] = (2.0 * c_outer) / denom;
}
}
Some(ErrorCorrection::new(shift, scale))
}
};
let metadata = Metadata {
vector_parameters: *vector_parameters,
bits,
mode,
error_correction: error_correction.as_ref().map(|ec| ErrorCorrectionMetadata {
shift: ec.shift.clone(),
scale: ec.scale.clone(),
}),
};
let quantizer = TurboQuantizer::new_from_metadata(&metadata).map_err(|e| {
EncodingError::EncodingError(format!(
"Failed to construct quantizer from metadata: {e}",
))
})?;
let mut buf = vec![0.0f64; quantizer.padded_dim];
for vector in data {
if stopped.load(Ordering::Relaxed) {
return Err(EncodingError::Stopped);
}
let encoded_vector: Vec<u8> =
Self::encode_vector(vector.as_ref(), &quantizer, &mut buf);
storage_builder
.push_vector_data(&encoded_vector)
.map_err(|e| {
EncodingError::EncodingError(format!("Failed to push encoded vector: {e}",))
})?;
}
let encoded_vectors = storage_builder
.build()
.map_err(|e| EncodingError::EncodingError(format!("Failed to build storage: {e}",)))?;
if let Some(meta_path) = meta_path {
meta_path
.parent()
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Path must have a parent directory",
)
})
.and_then(fs::create_dir_all)
.map_err(|e| {
EncodingError::EncodingError(format!(
"Failed to create metadata directory: {e}",
))
})?;
atomic_save_json(meta_path, &metadata).map_err(|e| {
EncodingError::EncodingError(format!("Failed to save metadata: {e}",))
})?;
}
Ok(Self {
encoded_vectors,
metadata,
metadata_path: meta_path.map(PathBuf::from),
encoding_buffer: vec![0.0f64; quantizer.padded_dim],
quantizer,
})
}
pub fn load(encoded_vectors: TStorage, meta_path: &Path) -> std::io::Result<Self> {
let contents = fs::read_to_string(meta_path)?;
let metadata: Metadata = serde_json::from_str(&contents)?;
let quantizer = TurboQuantizer::new_from_metadata(&metadata)?;
let result = Self {
encoded_vectors,
metadata,
metadata_path: Some(meta_path.to_path_buf()),
encoding_buffer: vec![0.0f64; quantizer.padded_dim],
quantizer,
};
Ok(result)
}
fn encode_vector(
vector_data: &[f32],
turbo_quantizer: &TurboQuantizer,
buf: &mut [f64],
) -> Vec<u8> {
turbo_quantizer.quantize(vector_data, buf)
}
pub fn get_quantized_vector(&self, i: PointOffsetType) -> Cow<'_, [u8]> {
self.encoded_vectors.get_vector_data(i)
}
pub fn layout(&self) -> Layout {
Layout::from_size_align(self.quantized_vector_size(), align_of::<f32>()).unwrap()
}
pub fn get_metadata(&self) -> &Metadata {
&self.metadata
}
}
pub fn get_quantized_vector_size(
vector_parameters: &VectorParameters,
bits: TQBits,
mode: TQMode,
) -> usize {
TurboQuantizer::quantized_size_for(
vector_parameters.dim,
bits,
vector_parameters.distance_type,
mode,
)
}
impl<TStorage: EncodedStorage> EncodedVectors for EncodedVectorsTQ<TStorage> {
type EncodedQuery = EncodedQueryTQ;
fn is_in_ram_or_mmap() -> bool {
TStorage::is_in_ram_or_mmap()
}
fn is_on_disk(&self) -> bool {
self.encoded_vectors.is_on_disk()
}
fn encode_query(&self, query: &[f32]) -> EncodedQueryTQ {
self.quantizer.precompute_query(query)
}
fn iter_batch(
&self,
offsets: &[PointOffsetType],
) -> impl Iterator<Item = (usize, Cow<'_, [u8]>)> {
self.encoded_vectors.iter_batch(offsets)
}
fn score(
&self,
query: &Self::EncodedQuery,
encoded_vector: &[u8],
hw_counter: &HardwareCounterCell,
) -> f32 {
self.score_bytes(True, query, encoded_vector, hw_counter)
}
fn score_point(
&self,
query: &EncodedQueryTQ,
i: PointOffsetType,
hw_counter: &HardwareCounterCell,
) -> f32 {
let encoded_vector = self.encoded_vectors.get_vector_data(i);
self.score_bytes(True, query, &encoded_vector, hw_counter)
}
fn score_internal(
&self,
i: PointOffsetType,
j: PointOffsetType,
hw_counter: &HardwareCounterCell,
) -> f32 {
let v1 = self.encoded_vectors.get_vector_data(i);
let v2 = self.encoded_vectors.get_vector_data(j);
hw_counter.vector_io_read().incr_delta(v1.len() + v2.len());
let score = self.quantizer.score_symmetric(&v1, &v2);
if self.metadata.vector_parameters.invert {
-score
} else {
score
}
}
fn quantized_vector_size(&self) -> usize {
get_quantized_vector_size(
&self.metadata.vector_parameters,
self.metadata.bits,
self.metadata.mode,
)
}
fn heap_size_bytes(&self) -> usize {
let Self {
encoded_vectors,
metadata: _,
metadata_path: _,
quantizer,
encoding_buffer,
} = self;
encoded_vectors.heap_size_bytes()
+ quantizer.heap_size_bytes()
+ encoding_buffer.capacity() * size_of::<f64>()
}
fn encode_internal_vector(&self, _id: PointOffsetType) -> Option<EncodedQueryTQ> {
None
}
fn upsert_vector(
&mut self,
id: PointOffsetType,
vector: &[f32],
hw_counter: &HardwareCounterCell,
) -> std::io::Result<()> {
let encoded_vector =
Self::encode_vector(vector, &self.quantizer, &mut self.encoding_buffer);
self.encoded_vectors.upsert_vector(
id,
bytemuck::cast_slice(encoded_vector.as_slice()),
hw_counter,
)
}
fn vectors_count(&self) -> usize {
self.encoded_vectors.vectors_count()
}
fn flusher(&self) -> MmapFlusher {
self.encoded_vectors.flusher()
}
fn files(&self) -> Vec<PathBuf> {
let mut files = self.encoded_vectors.files();
if let Some(meta_path) = &self.metadata_path {
files.push(meta_path.clone());
}
files
}
fn immutable_files(&self) -> Vec<PathBuf> {
let mut files = self.encoded_vectors.immutable_files();
if let Some(meta_path) = &self.metadata_path {
files.push(meta_path.clone());
}
files
}
type SupportsBytes = True;
fn score_bytes(
&self,
_: Self::SupportsBytes,
query: &Self::EncodedQuery,
bytes: &[u8],
hw_counter: &HardwareCounterCell,
) -> f32 {
hw_counter.cpu_counter().incr_delta(bytes.len());
let score = self.quantizer.score_precomputed(query, bytes);
if self.metadata.vector_parameters.invert {
-score
} else {
score
}
}
}