From fe5becdadd4e67ada5b85bbcd4417d32643858d4 Mon Sep 17 00:00:00 2001 From: Ivan Pleshkov Date: Thu, 19 Jun 2025 18:21:00 +0200 Subject: [PATCH] Merge pull request #6663 * bq encodings * are you happy clippy * are you happy clippy * are you happy clippy * are you happy clippy * gpu tests * update models * are you happy fmt * move additional bits to the end * fix tests * Welford's Algorithm * review remarks * are you happy clippy * remove debug println in test * coderabit nitpicks * remove unnecessary clone and partialeq * Use f64 for Welford's Algorithm * try fix ci * revert cargo-nextest * add debug assertions --- docs/grpc/docs.md | 15 ++ docs/redoc/master/openapi.json | 18 ++ lib/api/src/grpc/conversions.rs | 61 ++++- lib/api/src/grpc/proto/collections.proto | 7 + lib/api/src/grpc/qdrant.rs | 33 +++ lib/quantization/benches/binary.rs | 4 +- .../src/encoded_vectors_binary.rs | 235 +++++++++++++++++- lib/quantization/src/lib.rs | 1 + lib/quantization/src/vector_stats.rs | 97 ++++++++ .../tests/integration/empty_storage.rs | 1 + lib/quantization/tests/integration/main.rs | 2 + .../tests/integration/test_binary.rs | 14 +- .../integration/test_binary_encodings.rs | 120 +++++++++ .../gpu/gpu_vector_storage/tests.rs | 40 +-- lib/segment/src/types.rs | 18 ++ .../quantized/quantized_vectors.rs | 70 +++++- .../tests/custom_query_scorer_equivalency.rs | 1 + .../byte_storage_quantization_test.rs | 6 +- .../multivector_quantization_test.rs | 1 + 19 files changed, 697 insertions(+), 47 deletions(-) create mode 100644 lib/quantization/src/vector_stats.rs create mode 100644 lib/quantization/tests/integration/test_binary_encodings.rs diff --git a/docs/grpc/docs.md b/docs/grpc/docs.md index aad362c9ab..6bcf9e14dc 100644 --- a/docs/grpc/docs.md +++ b/docs/grpc/docs.md @@ -93,6 +93,7 @@ - [VectorsConfigDiff](#qdrant-VectorsConfigDiff) - [WalConfigDiff](#qdrant-WalConfigDiff) + - [BinaryQuantizationEncoding](#qdrant-BinaryQuantizationEncoding) - [CollectionStatus](#qdrant-CollectionStatus) - [CompressionRatio](#qdrant-CompressionRatio) - [Datatype](#qdrant-Datatype) @@ -395,6 +396,7 @@ | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | always_ram | [bool](#bool) | optional | If true - quantized vectors always will be stored in RAM, ignoring the config of main storage | +| encoding | [BinaryQuantizationEncoding](#qdrant-BinaryQuantizationEncoding) | optional | Binary quantization encoding method | @@ -1848,6 +1850,19 @@ Note: 1kB = 1 vector of size 256. | + + +### BinaryQuantizationEncoding + + +| Name | Number | Description | +| ---- | ------ | ----------- | +| OneBit | 0 | | +| TwoBits | 1 | | +| OneAndHalfBits | 2 | | + + + ### CollectionStatus diff --git a/docs/redoc/master/openapi.json b/docs/redoc/master/openapi.json index da2bc3bea1..e57deab16b 100644 --- a/docs/redoc/master/openapi.json +++ b/docs/redoc/master/openapi.json @@ -7016,9 +7016,27 @@ "always_ram": { "type": "boolean", "nullable": true + }, + "encoding": { + "anyOf": [ + { + "$ref": "#/components/schemas/BinaryQuantizationEncoding" + }, + { + "nullable": true + } + ] } } }, + "BinaryQuantizationEncoding": { + "type": "string", + "enum": [ + "one_bit", + "two_bits", + "one_and_half_bits" + ] + }, "Datatype": { "type": "string", "enum": [ diff --git a/lib/api/src/grpc/conversions.rs b/lib/api/src/grpc/conversions.rs index 2b64e87108..0e60e1e02e 100644 --- a/lib/api/src/grpc/conversions.rs +++ b/lib/api/src/grpc/conversions.rs @@ -35,7 +35,6 @@ use super::qdrant::{ }; use super::{Expression, Formula, RecoQuery, Usage}; use crate::conversions::json::{self, json_to_proto}; -use crate::grpc; use crate::grpc::qdrant::condition::ConditionOneOf; use crate::grpc::qdrant::r#match::MatchValue; use crate::grpc::qdrant::payload_index_params::IndexParams; @@ -55,7 +54,8 @@ use crate::grpc::qdrant::{ with_vectors_selector, }; use crate::grpc::{ - DecayParamsExpression, DivExpression, GeoDistance, MultExpression, PowExpression, SumExpression, + self, BinaryQuantizationEncoding, DecayParamsExpression, DivExpression, GeoDistance, + MultExpression, PowExpression, SumExpression, }; use crate::rest::models::{CollectionsResponse, VersionInfo}; use crate::rest::schema as rest; @@ -1080,11 +1080,50 @@ impl TryFrom for segment::types::ProductQuantization { } } +impl From for BinaryQuantizationEncoding { + fn from(value: segment::types::BinaryQuantizationEncoding) -> Self { + match value { + segment::types::BinaryQuantizationEncoding::OneBit => { + BinaryQuantizationEncoding::OneBit + } + segment::types::BinaryQuantizationEncoding::TwoBits => { + BinaryQuantizationEncoding::TwoBits + } + segment::types::BinaryQuantizationEncoding::OneAndHalfBits => { + BinaryQuantizationEncoding::OneAndHalfBits + } + } + } +} + +impl From for segment::types::BinaryQuantizationEncoding { + fn from(value: BinaryQuantizationEncoding) -> Self { + match value { + BinaryQuantizationEncoding::OneBit => { + segment::types::BinaryQuantizationEncoding::OneBit + } + BinaryQuantizationEncoding::TwoBits => { + segment::types::BinaryQuantizationEncoding::TwoBits + } + BinaryQuantizationEncoding::OneAndHalfBits => { + segment::types::BinaryQuantizationEncoding::OneAndHalfBits + } + } + } +} + impl From for BinaryQuantization { fn from(value: segment::types::BinaryQuantization) -> Self { let segment::types::BinaryQuantization { binary } = value; - let segment::types::BinaryQuantizationConfig { always_ram } = binary; - BinaryQuantization { always_ram } + let segment::types::BinaryQuantizationConfig { + always_ram, + encoding, + } = binary; + BinaryQuantization { + always_ram, + encoding: encoding + .map(|encoding| i32::from(BinaryQuantizationEncoding::from(encoding))), + } } } @@ -1092,9 +1131,19 @@ impl TryFrom for segment::types::BinaryQuantization { type Error = Status; fn try_from(value: BinaryQuantization) -> Result { - let BinaryQuantization { always_ram } = value; + let BinaryQuantization { + always_ram, + encoding, + } = value; + let encoding = encoding + .map(BinaryQuantizationEncoding::try_from) + .transpose() + .map_err(|_| Status::invalid_argument("Unknown binary quantization encoding"))?; Ok(segment::types::BinaryQuantization { - binary: segment::types::BinaryQuantizationConfig { always_ram }, + binary: segment::types::BinaryQuantizationConfig { + always_ram, + encoding: encoding.map(segment::types::BinaryQuantizationEncoding::from), + }, }) } } diff --git a/lib/api/src/grpc/proto/collections.proto b/lib/api/src/grpc/proto/collections.proto index 4e91f40a73..963457624a 100644 --- a/lib/api/src/grpc/proto/collections.proto +++ b/lib/api/src/grpc/proto/collections.proto @@ -295,8 +295,15 @@ message ProductQuantization { optional bool always_ram = 2; // If true - quantized vectors always will be stored in RAM, ignoring the config of main storage } +enum BinaryQuantizationEncoding { + OneBit = 0; + TwoBits = 1; + OneAndHalfBits = 2; +} + message BinaryQuantization { optional bool always_ram = 1; // If true - quantized vectors always will be stored in RAM, ignoring the config of main storage + optional BinaryQuantizationEncoding encoding = 2; // Binary quantization encoding method } message QuantizationConfig { diff --git a/lib/api/src/grpc/qdrant.rs b/lib/api/src/grpc/qdrant.rs index c759b06a83..94a6c42bc3 100644 --- a/lib/api/src/grpc/qdrant.rs +++ b/lib/api/src/grpc/qdrant.rs @@ -430,6 +430,9 @@ pub struct BinaryQuantization { /// If true - quantized vectors always will be stored in RAM, ignoring the config of main storage #[prost(bool, optional, tag = "1")] pub always_ram: ::core::option::Option, + /// Binary quantization encoding method + #[prost(enumeration = "BinaryQuantizationEncoding", optional, tag = "2")] + pub encoding: ::core::option::Option, } #[derive(validator::Validate)] #[derive(serde::Serialize)] @@ -1650,6 +1653,36 @@ impl CompressionRatio { #[derive(serde::Serialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] +pub enum BinaryQuantizationEncoding { + OneBit = 0, + TwoBits = 1, + OneAndHalfBits = 2, +} +impl BinaryQuantizationEncoding { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + BinaryQuantizationEncoding::OneBit => "OneBit", + BinaryQuantizationEncoding::TwoBits => "TwoBits", + BinaryQuantizationEncoding::OneAndHalfBits => "OneAndHalfBits", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "OneBit" => Some(Self::OneBit), + "TwoBits" => Some(Self::TwoBits), + "OneAndHalfBits" => Some(Self::OneAndHalfBits), + _ => None, + } + } +} +#[derive(serde::Serialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] pub enum ShardingMethod { /// Auto-sharding based on record ids Auto = 0, diff --git a/lib/quantization/benches/binary.rs b/lib/quantization/benches/binary.rs index 044cc6ceb9..126102d3bd 100644 --- a/lib/quantization/benches/binary.rs +++ b/lib/quantization/benches/binary.rs @@ -4,7 +4,7 @@ use common::counter::hardware_counter::HardwareCounterCell; use criterion::{Criterion, criterion_group, criterion_main}; use permutation_iterator::Permutor; use quantization::encoded_vectors::{DistanceType, EncodedVectors, VectorParameters}; -use quantization::encoded_vectors_binary::EncodedVectorsBin; +use quantization::encoded_vectors_binary::{EncodedVectorsBin, Encoding}; use rand::{Rng, SeedableRng}; fn generate_number(rng: &mut rand::rngs::StdRng) -> f32 { @@ -39,6 +39,7 @@ fn binary_bench(c: &mut Criterion) { distance_type: DistanceType::Dot, invert: false, }, + Encoding::OneBit, &AtomicBool::new(false), ) .unwrap(); @@ -78,6 +79,7 @@ fn binary_bench(c: &mut Criterion) { distance_type: DistanceType::Dot, invert: false, }, + Encoding::OneBit, &AtomicBool::new(false), ) .unwrap(); diff --git a/lib/quantization/src/encoded_vectors_binary.rs b/lib/quantization/src/encoded_vectors_binary.rs index 53b18900ba..fb246d6480 100644 --- a/lib/quantization/src/encoded_vectors_binary.rs +++ b/lib/quantization/src/encoded_vectors_binary.rs @@ -1,5 +1,5 @@ use std::marker::PhantomData; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use common::counter::hardware_counter::HardwareCounterCell; @@ -8,6 +8,7 @@ use memory::mmap_ops::{transmute_from_u8_to_slice, transmute_to_u8_slice}; use serde::{Deserialize, Serialize}; use crate::encoded_vectors::validate_vector_parameters; +use crate::vector_stats::VectorStats; use crate::{ DistanceType, EncodedStorage, EncodedStorageBuilder, EncodedVectors, EncodingError, VectorParameters, @@ -16,9 +17,24 @@ use crate::{ pub struct EncodedVectorsBin { encoded_vectors: TStorage, metadata: Metadata, + vector_stats: Option, bits_store_type: PhantomData, } +#[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize, Default)] +pub enum Encoding { + #[default] + OneBit, + TwoBits, + OneAndHalfBits, +} + +impl Encoding { + pub fn is_one(&self) -> bool { + matches!(self, Encoding::OneBit) + } +} + pub struct EncodedBinVector { encoded_vector: Vec, } @@ -26,6 +42,9 @@ pub struct EncodedBinVector { #[derive(Serialize, Deserialize)] struct Metadata { vector_parameters: VectorParameters, + #[serde(default)] + #[serde(skip_serializing_if = "Encoding::is_one")] + encoding: Encoding, } pub trait BitsStoreType: @@ -155,7 +174,7 @@ impl BitsStoreType for u128 { } fn get_storage_size(size: usize) -> usize { - let bits_count = 8 * std::mem::size_of::(); + let bits_count = u8::BITS as usize * std::mem::size_of::(); let mut result = size / bits_count; if size % bits_count != 0 { result += 1; @@ -175,16 +194,24 @@ impl orig_data: impl Iterator + 'a> + Clone, mut storage_builder: impl EncodedStorageBuilder, vector_parameters: &VectorParameters, + encoding: Encoding, stopped: &AtomicBool, ) -> Result { debug_assert!(validate_vector_parameters(orig_data.clone(), vector_parameters).is_ok()); + let vector_stats = match encoding { + Encoding::OneBit => None, + Encoding::TwoBits | Encoding::OneAndHalfBits => { + Some(VectorStats::build(orig_data.clone(), vector_parameters)) + } + }; + for vector in orig_data { if stopped.load(Ordering::Relaxed) { return Err(EncodingError::Stopped); } - let encoded_vector = Self::encode_vector(vector.as_ref()); + let encoded_vector = Self::encode_vector(vector.as_ref(), &vector_stats, encoding); let encoded_vector_slice = encoded_vector.encoded_vector.as_slice(); let bytes = transmute_to_u8_slice(encoded_vector_slice); storage_builder.push_vector_data(bytes); @@ -194,15 +221,37 @@ impl encoded_vectors: storage_builder.build(), metadata: Metadata { vector_parameters: vector_parameters.clone(), + encoding, }, + vector_stats, bits_store_type: PhantomData, }) } - fn encode_vector(vector: &[f32]) -> EncodedBinVector { - let mut encoded_vector = - vec![Default::default(); TBitsStoreType::get_storage_size(vector.len())]; + fn encode_vector( + vector: &[f32], + vector_stats: &Option, + encoding: Encoding, + ) -> EncodedBinVector { + let encoded_vector_size = + Self::get_quantized_vector_size_from_params(vector.len(), encoding) + / std::mem::size_of::(); + let mut encoded_vector = vec![Default::default(); encoded_vector_size]; + match encoding { + Encoding::OneBit => Self::encode_one_bit_vector(vector, &mut encoded_vector), + Encoding::TwoBits => { + Self::encode_two_bits_vector(vector, &mut encoded_vector, vector_stats) + } + Encoding::OneAndHalfBits => { + Self::encode_one_and_half_bits_vector(vector, &mut encoded_vector, vector_stats) + } + } + + EncodedBinVector { encoded_vector } + } + + fn encode_one_bit_vector(vector: &[f32], encoded_vector: &mut [TBitsStoreType]) { let bits_count = u8::BITS as usize * std::mem::size_of::(); let one = TBitsStoreType::one(); for (i, &v) in vector.iter().enumerate() { @@ -212,17 +261,137 @@ impl encoded_vector[i / bits_count] |= one << (i % bits_count); } } - - EncodedBinVector { encoded_vector } } - pub fn get_quantized_vector_size_from_params(vector_parameters: &VectorParameters) -> usize { - TBitsStoreType::get_storage_size(vector_parameters.dim) + fn encode_two_bits_vector( + vector: &[f32], + encoded_vector: &mut [TBitsStoreType], + vector_stats: &Option, + ) { + let Some(vector_stats) = vector_stats else { + debug_assert!(false, "Vector stats must be provided for two bits encoding"); + // If vector stats are not provided, we cannot encode two bits + // So we fall back to one bit encoding + return Self::encode_one_bit_vector(vector, encoded_vector); + }; + + let bits_count = u8::BITS as usize * std::mem::size_of::(); + let one = TBitsStoreType::one(); + for (i, &v) in vector.iter().enumerate() { + let mean = vector_stats.elements_stats[i].mean; + let sd = vector_stats.elements_stats[i].stddev; + + let (b1, b2) = Self::encode_two_bits_value(v, mean, sd); + + if b1 { + encoded_vector[i / bits_count] |= one << (i % bits_count); + } + if b2 { + let j = vector.len() + i; + encoded_vector[j / bits_count] |= one << (j % bits_count); + } + } + } + + fn encode_one_and_half_bits_vector( + vector: &[f32], + encoded_vector: &mut [TBitsStoreType], + vector_stats: &Option, + ) { + let Some(vector_stats) = vector_stats else { + debug_assert!(false, "Vector stats must be provided for two bits encoding"); + // If vector stats are not provided, we cannot encode one and half bits + // So we fall back to one bit encoding + return Self::encode_one_bit_vector(vector, encoded_vector); + }; + + // One and half bit encoding is a 2bit quantization but first bit, + // which describes that value is less that sigma, + // is united with the bit from the next value using OR operand. + // Scoring for 1.5bit quantization is the same as for 2bit and 1bit quantization. + // + // Example 1: + // `Value1` has `[1,0]` 2bits encoding, value `Value2` has `[1,1]` 2bits encoding. + // The resulting 1.5bit encoding will be `[value1[0], value2[0], value1[1] | value2[1]] = [1, 1, 1]`. + // + // Example 2: + // `Value1` has `[0,0]` 2bits encoding, value `Value2` has `[1,0]` 2bits encoding. + // The resulting 1.5bit encoding will be `[value1[0], value2[0], value1[1] | value2[1]] = [0, 1, 0]`. + let bits_count = u8::BITS as usize * std::mem::size_of::(); + let one = TBitsStoreType::one(); + for (i, &v) in vector.iter().enumerate() { + let mean = vector_stats.elements_stats[i].mean; + let sd = vector_stats.elements_stats[i].stddev; + let (b1, b2) = Self::encode_two_bits_value(v, mean, sd); + + if b1 { + encoded_vector[i / bits_count] |= one << (i % bits_count); + } + if b2 { + let j = vector.len() + i / 2; + encoded_vector[j / bits_count] |= one << (j % bits_count); + } + } + } + + fn encode_two_bits_value(value: f32, mean: f32, sd: f32) -> (bool, bool) { + // Two bit encoding is a regular BQ with "zero". + // It uses 2 bits per value and encodes values in the following way: + // 00 - if the value is in the range [-2*sigma; -sigma); + // 10 - if the value is in the range [-sigma; sigma); + // 11 - if the value is in the range [sigma; 2*sigma]; + // where sigma is the standard deviation of the value. + // + // Scoring for 2bit quantization is the same as for 1bit quantization. + + if sd < f32::EPSILON { + // If standard deviation is zero, + // we cannot calculate z-score count so use regular BQ with zero-comparison. + return (value > 0.0, false); + } + + // How many ranges we want to divide the values into, it's 3: + // [-2*sigma; -sigma), [-sigma; sigma), [sigma; 2*sigma) + let ranges = 3; + + // Calculate z-score for the value + let v_z = (value - mean) / sd; + + let min_border = -2.0; // -2*sigma + let max_border = 2.0; // 2*sigma + + // Normalize z-score to the range [-2*sigma; 2*sigma] + let normalized_z = (v_z - min_border) / (max_border - min_border); + + // Calculate index in the ranges list: [-2*sigma; -sigma), [-sigma; sigma), [sigma; 2*sigma) + let index = normalized_z * (ranges as f32); + + // Index 0 and less is [0, 0] encoding + // Index 1 is [1, 0] encoding + // Index 2 and more is [1, 1] encoding + if index >= 1.0 { + let count_ones = (index.floor() as usize).min(2); + (count_ones > 0, count_ones > 1) + } else { + (false, false) + } + } + + pub fn get_quantized_vector_size_from_params(dim: usize, encoding: Encoding) -> usize { + let extended_dim = match encoding { + Encoding::OneBit => dim, + Encoding::TwoBits => dim * 2, + Encoding::OneAndHalfBits => (dim * 3).div_ceil(2), // ceil(dim * 1.5) + }; + TBitsStoreType::get_storage_size(extended_dim.max(1)) * std::mem::size_of::() } fn get_quantized_vector_size(&self) -> usize { - Self::get_quantized_vector_size_from_params(&self.metadata.vector_parameters) + Self::get_quantized_vector_size_from_params( + self.metadata.vector_parameters.dim, + self.metadata.encoding, + ) } fn calculate_metric(&self, v1: &[TBitsStoreType], v2: &[TBitsStoreType]) -> f32 { @@ -273,6 +442,12 @@ impl pub fn vectors_count(&self) -> usize { self.metadata.vector_parameters.count } + + fn get_vector_stats_path_from_meta_path(meta_path: &Path) -> Option { + let mut vector_stats_path = meta_path.parent()?.to_owned(); + vector_stats_path.push("vector_stats.json"); + Some(vector_stats_path) + } } impl EncodedVectors @@ -286,6 +461,21 @@ impl EncodedVectors data_path.parent().map(std::fs::create_dir_all); self.encoded_vectors.save_to_file(data_path)?; + + let vector_stats_path = + Self::get_vector_stats_path_from_meta_path(meta_path).ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Failed to get vector stats path", + ) + })?; + if let Some(vector_stats) = &self.vector_stats { + vector_stats_path.parent().map(std::fs::create_dir_all); + atomic_save_json(&vector_stats_path, &vector_stats)?; + } else if let Ok(true) = std::fs::exists(&vector_stats_path) { + std::fs::remove_file(&vector_stats_path)?; + } + Ok(()) } @@ -296,12 +486,31 @@ impl EncodedVectors ) -> std::io::Result { let contents = std::fs::read_to_string(meta_path)?; let metadata: Metadata = serde_json::from_str(&contents)?; - let quantized_vector_size = Self::get_quantized_vector_size_from_params(vector_parameters); + let quantized_vector_size = + Self::get_quantized_vector_size_from_params(vector_parameters.dim, metadata.encoding); let encoded_vectors = TStorage::from_file(data_path, quantized_vector_size, vector_parameters.count)?; + + let vector_stats_path = + Self::get_vector_stats_path_from_meta_path(meta_path).ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Failed to get vector stats path", + ) + })?; + let vector_stats = match metadata.encoding { + Encoding::OneBit => None, + Encoding::TwoBits | Encoding::OneAndHalfBits => { + let vector_stats_contents = std::fs::read_to_string(&vector_stats_path)?; + let vector_stats: VectorStats = serde_json::from_str(&vector_stats_contents)?; + Some(vector_stats) + } + }; + let result = Self { metadata, encoded_vectors, + vector_stats, bits_store_type: PhantomData, }; Ok(result) @@ -313,7 +522,7 @@ impl EncodedVectors fn encode_query(&self, query: &[f32]) -> EncodedBinVector { debug_assert!(query.len() == self.metadata.vector_parameters.dim); - Self::encode_vector(query) + Self::encode_vector(query, &self.vector_stats, self.metadata.encoding) } fn score_point( diff --git a/lib/quantization/src/lib.rs b/lib/quantization/src/lib.rs index 655226eee6..3449dec9da 100644 --- a/lib/quantization/src/lib.rs +++ b/lib/quantization/src/lib.rs @@ -5,6 +5,7 @@ pub mod encoded_vectors_pq; pub mod encoded_vectors_u8; pub mod kmeans; pub mod quantile; +pub mod vector_stats; use std::fmt::Display; use std::sync::{Arc, Condvar, Mutex}; diff --git a/lib/quantization/src/vector_stats.rs b/lib/quantization/src/vector_stats.rs new file mode 100644 index 0000000000..c2ae91d31b --- /dev/null +++ b/lib/quantization/src/vector_stats.rs @@ -0,0 +1,97 @@ +use serde::{Deserialize, Serialize}; + +use crate::VectorParameters; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VectorStats { + pub elements_stats: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VectorElementStats { + pub min: f32, + pub max: f32, + pub mean: f32, + pub stddev: f32, +} + +impl Default for VectorElementStats { + fn default() -> Self { + VectorElementStats { + min: f32::MAX, + max: f32::MIN, + mean: 0.0, + stddev: 0.0, + } + } +} + +impl VectorStats { + pub fn build<'a>( + data: impl Iterator + 'a>, + vector_params: &VectorParameters, + ) -> Self { + // The Welford's Algorithm. + let mut stats = VectorStats { + elements_stats: vec![VectorElementStats::default(); vector_params.dim], + }; + + // For internal calculations use higher precision. + let mut m2 = vec![0.0f64; vector_params.dim]; + let mut means = vec![0.0f64; vector_params.dim]; + + let mut count = 0; + for vector in data { + let vector = vector.as_ref(); + count += 1; + + debug_assert_eq!( + vector.len(), + vector_params.dim, + "Vector length does not match the expected dimension" + ); + + for (((&value, element_stats), mean), m2) in vector + .iter() + .zip(stats.elements_stats.iter_mut()) + .zip(means.iter_mut()) + .zip(m2.iter_mut()) + { + element_stats.min = if value < element_stats.min { + value + } else { + element_stats.min + }; + element_stats.max = if value > element_stats.max { + value + } else { + element_stats.max + }; + + let delta = f64::from(value) - *mean; + *mean += delta / count as f64; + *m2 += delta * (f64::from(value) - *mean); + } + } + + debug_assert_eq!( + count, vector_params.count, + "Count of vectors processed does not match the expected count in vector parameters" + ); + for ((element_stats, means), m2) in stats + .elements_stats + .iter_mut() + .zip(means.iter()) + .zip(m2.iter()) + { + element_stats.stddev = if count > 1 { + (*m2 / (count - 1) as f64).sqrt() as f32 + } else { + 0.0 + }; + element_stats.mean = *means as f32; + } + + stats + } +} diff --git a/lib/quantization/tests/integration/empty_storage.rs b/lib/quantization/tests/integration/empty_storage.rs index f44e8e5e63..e1f4285f5f 100644 --- a/lib/quantization/tests/integration/empty_storage.rs +++ b/lib/quantization/tests/integration/empty_storage.rs @@ -101,6 +101,7 @@ mod tests { vector_data.iter(), Vec::::new(), &vector_parameters, + quantization::encoded_vectors_binary::Encoding::OneBit, &AtomicBool::new(false), ) .unwrap(); diff --git a/lib/quantization/tests/integration/main.rs b/lib/quantization/tests/integration/main.rs index b0766e443a..bf3cbb72f0 100644 --- a/lib/quantization/tests/integration/main.rs +++ b/lib/quantization/tests/integration/main.rs @@ -9,6 +9,8 @@ pub mod test_avx2; #[cfg(test)] pub mod test_binary; #[cfg(test)] +pub mod test_binary_encodings; +#[cfg(test)] pub mod test_neon; #[cfg(test)] pub mod test_pq; diff --git a/lib/quantization/tests/integration/test_binary.rs b/lib/quantization/tests/integration/test_binary.rs index b2ffbb563c..0f69985754 100644 --- a/lib/quantization/tests/integration/test_binary.rs +++ b/lib/quantization/tests/integration/test_binary.rs @@ -4,7 +4,7 @@ mod tests { use common::counter::hardware_counter::HardwareCounterCell; use quantization::encoded_vectors::{DistanceType, EncodedVectors, VectorParameters}; - use quantization::encoded_vectors_binary::{BitsStoreType, EncodedVectorsBin}; + use quantization::encoded_vectors_binary::{BitsStoreType, EncodedVectorsBin, Encoding}; use rand::{Rng, SeedableRng}; use crate::metrics::{dot_similarity, l1_similarity, l2_similarity}; @@ -49,6 +49,7 @@ mod tests { distance_type: DistanceType::Dot, invert: false, }, + Encoding::OneBit, &AtomicBool::new(false), ) .unwrap(); @@ -95,6 +96,7 @@ mod tests { distance_type: DistanceType::Dot, invert: true, }, + Encoding::OneBit, &AtomicBool::new(false), ) .unwrap(); @@ -141,6 +143,7 @@ mod tests { distance_type: DistanceType::Dot, invert: false, }, + Encoding::OneBit, &AtomicBool::new(false), ) .unwrap(); @@ -184,6 +187,7 @@ mod tests { distance_type: DistanceType::Dot, invert: true, }, + Encoding::OneBit, &AtomicBool::new(false), ) .unwrap(); @@ -226,6 +230,7 @@ mod tests { distance_type: DistanceType::L1, invert: false, }, + Encoding::OneBit, &AtomicBool::new(false), ) .unwrap(); @@ -287,6 +292,7 @@ mod tests { distance_type: DistanceType::L1, invert: true, }, + Encoding::OneBit, &AtomicBool::new(false), ) .unwrap(); @@ -348,6 +354,7 @@ mod tests { distance_type: DistanceType::L1, invert: false, }, + Encoding::OneBit, &AtomicBool::new(false), ) .unwrap(); @@ -406,6 +413,7 @@ mod tests { distance_type: DistanceType::L1, invert: true, }, + Encoding::OneBit, &AtomicBool::new(false), ) .unwrap(); @@ -464,6 +472,7 @@ mod tests { distance_type: DistanceType::L2, invert: false, }, + Encoding::OneBit, &AtomicBool::new(false), ) .unwrap(); @@ -525,6 +534,7 @@ mod tests { distance_type: DistanceType::L2, invert: true, }, + Encoding::OneBit, &AtomicBool::new(false), ) .unwrap(); @@ -586,6 +596,7 @@ mod tests { distance_type: DistanceType::L2, invert: false, }, + Encoding::OneBit, &AtomicBool::new(false), ) .unwrap(); @@ -644,6 +655,7 @@ mod tests { distance_type: DistanceType::L2, invert: true, }, + Encoding::OneBit, &AtomicBool::new(false), ) .unwrap(); diff --git a/lib/quantization/tests/integration/test_binary_encodings.rs b/lib/quantization/tests/integration/test_binary_encodings.rs new file mode 100644 index 0000000000..ceafb450e6 --- /dev/null +++ b/lib/quantization/tests/integration/test_binary_encodings.rs @@ -0,0 +1,120 @@ +#[cfg(test)] +mod tests { + use std::sync::atomic::AtomicBool; + + use common::counter::hardware_counter::HardwareCounterCell; + use quantization::encoded_vectors::{DistanceType, EncodedVectors, VectorParameters}; + use quantization::encoded_vectors_binary::{BitsStoreType, EncodedVectorsBin, Encoding}; + use rand::{Rng, SeedableRng}; + + use crate::metrics::dot_similarity; + + fn generate_number(rng: &mut rand::rngs::StdRng) -> f32 { + rng.random_range(-1.0..1.0) + } + + fn generate_vector(dim: usize, rng: &mut rand::rngs::StdRng) -> Vec { + (0..dim) + .map(|_| generate_number(rng) / (dim as f32).sqrt()) + .collect() + } + + fn get_top(scores: &[f32], count: usize, invert: bool) -> Vec { + let mut indices: Vec = (0..scores.len()).collect(); + indices.sort_by(|&a, &b| scores[b].partial_cmp(&scores[a]).unwrap()); + if invert { + indices.reverse(); + } + indices.into_iter().take(count).collect() + } + + fn match_count(ids1: &[usize], ids2: &[usize]) -> usize { + ids1.iter().filter(|&&id| ids2.contains(&id)).count() + } + + #[test] + fn test_binary_dot() { + test_binary_dot_impl::(0, false); + test_binary_dot_impl::(600, false); + test_binary_dot_impl::(601, false); + test_binary_dot_impl::(600, false); + } + + #[test] + fn test_binary_dot_inverted() { + test_binary_dot_impl::(700, true); + test_binary_dot_impl::(700, true); + } + + fn test_binary_dot_impl(vector_dim: usize, invert: bool) { + let vectors_count = 1000; + + let mut rng = rand::rngs::StdRng::seed_from_u64(42); + let mut vector_data: Vec> = Vec::new(); + for _ in 0..vectors_count { + vector_data.push(generate_vector(vector_dim, &mut rng)); + } + + let encodings = [ + Encoding::OneBit, + Encoding::OneAndHalfBits, + Encoding::TwoBits, + ]; + + let encoded: Vec<_> = encodings + .iter() + .map(|&encoding| { + EncodedVectorsBin::::encode( + vector_data.iter(), + Vec::::new(), + &VectorParameters { + dim: vector_dim, + count: vectors_count, + distance_type: DistanceType::Dot, + invert, + }, + encoding, + &AtomicBool::new(false), + ) + .unwrap() + }) + .collect(); + + let top = 10; + let query: Vec = generate_vector(vector_dim, &mut rng); + + let orig_scores: Vec = vector_data + .iter() + .map(|vector| dot_similarity(&query, vector)) + .collect(); + let original_top = get_top(&orig_scores, top, invert); + + let tops = encoded + .iter() + .map(|encoded| { + let query_encoded = encoded.encode_query(&query); + let scores: Vec = (0..vector_data.len()) + .map(|index| { + encoded.score_point( + &query_encoded, + index as u32, + &HardwareCounterCell::new(), + ) + }) + .collect(); + let tops = get_top(&scores, top, false); + match_count(&original_top, &tops) + }) + .collect::>(); + + // Check if encoding has more accuracy than previous one + for i in 1..tops.len() { + assert!( + tops[i] >= tops[i - 1], + "Encoding {} has less accuracy than encoding {}", + i, + i - 1 + ); + } + } +} diff --git a/lib/segment/src/index/hnsw_index/gpu/gpu_vector_storage/tests.rs b/lib/segment/src/index/hnsw_index/gpu/gpu_vector_storage/tests.rs index 50ebfd6913..32f9025656 100644 --- a/lib/segment/src/index/hnsw_index/gpu/gpu_vector_storage/tests.rs +++ b/lib/segment/src/index/hnsw_index/gpu/gpu_vector_storage/tests.rs @@ -18,8 +18,9 @@ use crate::index::hnsw_index::gpu::shader_builder::ShaderBuilder; use crate::spaces::metric::Metric; use crate::spaces::simple::{CosineMetric, DotProductMetric, EuclidMetric, ManhattanMetric}; use crate::types::{ - BinaryQuantization, BinaryQuantizationConfig, Distance, ProductQuantization, - ProductQuantizationConfig, QuantizationConfig, ScalarQuantization, ScalarQuantizationConfig, + BinaryQuantization, BinaryQuantizationConfig, BinaryQuantizationEncoding, Distance, + ProductQuantization, ProductQuantizationConfig, QuantizationConfig, ScalarQuantization, + ScalarQuantizationConfig, }; use crate::vector_storage::dense::simple_dense_vector_storage::{ open_simple_dense_byte_vector_storage, open_simple_dense_full_vector_storage, @@ -144,65 +145,75 @@ fn test_gpu_vector_storage_sq( } #[rstest] -#[case::cosine_f32( +#[case::cosine_f32_one_bit( Distance::Cosine, TestStorageType::Dense(TestElementType::Float32), 273, - 2057 + 2057, + BinaryQuantizationEncoding::OneBit )] -#[case::dot_f32( +#[case::dot_f32_one_and_half_bits( Distance::Dot, TestStorageType::Dense(TestElementType::Float32), 256, - 512 + 512, + BinaryQuantizationEncoding::OneAndHalfBits )] #[case::euclid_f32( Distance::Euclid, TestStorageType::Dense(TestElementType::Float32), 273, - 2057 + 2057, + BinaryQuantizationEncoding::OneBit )] -#[case::manhattan_f32( +#[case::manhattan_f32_two_bits( Distance::Manhattan, TestStorageType::Dense(TestElementType::Float32), 273, - 2057 + 2057, + BinaryQuantizationEncoding::TwoBits )] #[case::small_dimension( Distance::Cosine, TestStorageType::Dense(TestElementType::Float32), 17, - 2057 + 2057, + BinaryQuantizationEncoding::OneBit )] #[case::cosine_f16( Distance::Cosine, TestStorageType::Dense(TestElementType::Float16), 273, - 2057 + 2057, + BinaryQuantizationEncoding::OneBit )] #[case::cosine_u8( Distance::Cosine, TestStorageType::Dense(TestElementType::Uint8), 273, - 2057 + 2057, + BinaryQuantizationEncoding::OneBit )] #[case::cosine_multi_f32( Distance::Cosine, TestStorageType::Multi(TestElementType::Float32), 67, - 2057 + 2057, + BinaryQuantizationEncoding::OneBit )] #[case::cosine_multi_u8( Distance::Cosine, TestStorageType::Multi(TestElementType::Uint8), 273, - 2057 + 2057, + BinaryQuantizationEncoding::OneBit )] fn test_gpu_vector_storage_bq( #[case] distance: Distance, #[case] storage_type: TestStorageType, #[case] dim: usize, #[case] num_vectors: usize, + #[case] encoding: BinaryQuantizationEncoding, ) { let _ = env_logger::builder() .is_test(true) @@ -212,6 +223,7 @@ fn test_gpu_vector_storage_bq( let quantization_config = QuantizationConfig::Binary(BinaryQuantization { binary: BinaryQuantizationConfig { always_ram: Some(true), + encoding: Some(encoding), }, }); diff --git a/lib/segment/src/types.rs b/lib/segment/src/types.rs index 0223f79a84..87d7b5b926 100644 --- a/lib/segment/src/types.rs +++ b/lib/segment/src/types.rs @@ -660,11 +660,29 @@ impl Hash for ScalarQuantizationConfig { impl Eq for ScalarQuantizationConfig {} +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, PartialEq, Eq, Hash, Default)] +#[serde(rename_all = "snake_case")] +pub enum BinaryQuantizationEncoding { + #[default] + OneBit, + TwoBits, + OneAndHalfBits, +} + +impl BinaryQuantizationEncoding { + pub fn is_one_bit(&self) -> bool { + matches!(self, BinaryQuantizationEncoding::OneBit) + } +} + #[derive(Debug, Deserialize, Serialize, JsonSchema, Validate, Clone, PartialEq, Eq, Hash)] #[serde(rename_all = "snake_case")] pub struct BinaryQuantizationConfig { #[serde(skip_serializing_if = "Option::is_none")] pub always_ram: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub encoding: Option, } #[derive(Debug, Deserialize, Serialize, JsonSchema, Validate, Clone, PartialEq, Eq, Hash)] diff --git a/lib/segment/src/vector_storage/quantized/quantized_vectors.rs b/lib/segment/src/vector_storage/quantized/quantized_vectors.rs index f267dcf091..7ceff8abc6 100644 --- a/lib/segment/src/vector_storage/quantized/quantized_vectors.rs +++ b/lib/segment/src/vector_storage/quantized/quantized_vectors.rs @@ -20,9 +20,9 @@ use crate::common::vector_utils::TrySetCapacityExact; use crate::data_types::primitive::PrimitiveVectorElement; use crate::data_types::vectors::{QueryVector, VectorElementType}; use crate::types::{ - BinaryQuantization, BinaryQuantizationConfig, CompressionRatio, Distance, MultiVectorConfig, - ProductQuantization, ProductQuantizationConfig, QuantizationConfig, ScalarQuantization, - ScalarQuantizationConfig, VectorStorageDatatype, + BinaryQuantization, BinaryQuantizationConfig, BinaryQuantizationEncoding, CompressionRatio, + Distance, MultiVectorConfig, ProductQuantization, ProductQuantizationConfig, + QuantizationConfig, ScalarQuantization, ScalarQuantizationConfig, VectorStorageDatatype, }; use crate::vector_storage::chunked_vectors::ChunkedVectors; use crate::vector_storage::quantized::quantized_mmap_storage::{ @@ -862,16 +862,35 @@ impl QuantizedVectors { on_disk_vector_storage: bool, stopped: &AtomicBool, ) -> OperationResult { + let encoding = match binary_config.encoding { + Some(BinaryQuantizationEncoding::OneBit) => { + quantization::encoded_vectors_binary::Encoding::OneBit + } + Some(BinaryQuantizationEncoding::TwoBits) => { + quantization::encoded_vectors_binary::Encoding::TwoBits + } + Some(BinaryQuantizationEncoding::OneAndHalfBits) => { + quantization::encoded_vectors_binary::Encoding::OneAndHalfBits + } + None => quantization::encoded_vectors_binary::Encoding::OneBit, + }; let quantized_vector_size = EncodedVectorsBin::::get_quantized_vector_size_from_params( - vector_parameters, + vector_parameters.dim, + encoding, ); let in_ram = Self::is_ram(binary_config.always_ram, on_disk_vector_storage); if in_ram { let mut storage_builder = ChunkedVectors::::new(quantized_vector_size); storage_builder.try_set_capacity_exact(vector_parameters.count)?; Ok(QuantizedVectorStorage::BinaryRam( - EncodedVectorsBin::encode(vectors, storage_builder, vector_parameters, stopped)?, + EncodedVectorsBin::encode( + vectors, + storage_builder, + vector_parameters, + encoding, + stopped, + )?, )) } else { let mmap_data_path = path.join(QUANTIZED_DATA_PATH); @@ -881,7 +900,13 @@ impl QuantizedVectors { quantized_vector_size, )?; Ok(QuantizedVectorStorage::BinaryMmap( - EncodedVectorsBin::encode(vectors, storage_builder, vector_parameters, stopped)?, + EncodedVectorsBin::encode( + vectors, + storage_builder, + vector_parameters, + encoding, + stopped, + )?, )) } } @@ -897,16 +922,34 @@ impl QuantizedVectors { on_disk_vector_storage: bool, stopped: &AtomicBool, ) -> OperationResult { + let encoding = match binary_config.encoding { + Some(BinaryQuantizationEncoding::OneBit) => { + quantization::encoded_vectors_binary::Encoding::OneBit + } + Some(BinaryQuantizationEncoding::TwoBits) => { + quantization::encoded_vectors_binary::Encoding::TwoBits + } + Some(BinaryQuantizationEncoding::OneAndHalfBits) => { + quantization::encoded_vectors_binary::Encoding::OneAndHalfBits + } + None => quantization::encoded_vectors_binary::Encoding::OneBit, + }; let quantized_vector_size = EncodedVectorsBin::::get_quantized_vector_size_from_params( - vector_parameters, + vector_parameters.dim, + encoding, ); let in_ram = Self::is_ram(binary_config.always_ram, on_disk_vector_storage); if in_ram { let mut storage_builder = ChunkedVectors::::new(quantized_vector_size); storage_builder.try_set_capacity_exact(vector_parameters.count)?; - let quantized_storage = - EncodedVectorsBin::encode(vectors, storage_builder, vector_parameters, stopped)?; + let quantized_storage = EncodedVectorsBin::encode( + vectors, + storage_builder, + vector_parameters, + encoding, + stopped, + )?; Ok(QuantizedVectorStorage::BinaryRamMulti( QuantizedMultivectorStorage::new( vector_parameters.dim, @@ -922,8 +965,13 @@ impl QuantizedVectors { vector_parameters.count, quantized_vector_size, )?; - let quantized_storage = - EncodedVectorsBin::encode(vectors, storage_builder, vector_parameters, stopped)?; + let quantized_storage = EncodedVectorsBin::encode( + vectors, + storage_builder, + vector_parameters, + encoding, + stopped, + )?; let offsets_path = path.join(QUANTIZED_OFFSETS_PATH); create_offsets_file_from_iter(&offsets_path, vector_parameters.count, offsets)?; Ok(QuantizedVectorStorage::BinaryMmapMulti( diff --git a/lib/segment/src/vector_storage/tests/custom_query_scorer_equivalency.rs b/lib/segment/src/vector_storage/tests/custom_query_scorer_equivalency.rs index dd1aec626c..ab9e800b34 100644 --- a/lib/segment/src/vector_storage/tests/custom_query_scorer_equivalency.rs +++ b/lib/segment/src/vector_storage/tests/custom_query_scorer_equivalency.rs @@ -93,6 +93,7 @@ fn product_x4() -> WithQuantization { fn binary() -> WithQuantization { let config = BinaryQuantizationConfig { always_ram: Some(true), + encoding: None, } .into(); diff --git a/lib/segment/tests/integration/byte_storage_quantization_test.rs b/lib/segment/tests/integration/byte_storage_quantization_test.rs index 9928ef76a1..3694aa732f 100644 --- a/lib/segment/tests/integration/byte_storage_quantization_test.rs +++ b/lib/segment/tests/integration/byte_storage_quantization_test.rs @@ -297,7 +297,11 @@ fn test_byte_storage_binary_quantization_hnsw( always_ram: None, } .into(), - QuantizationVariant::Binary => BinaryQuantizationConfig { always_ram: None }.into(), + QuantizationVariant::Binary => BinaryQuantizationConfig { + always_ram: None, + encoding: None, + } + .into(), }; segment_byte diff --git a/lib/segment/tests/integration/multivector_quantization_test.rs b/lib/segment/tests/integration/multivector_quantization_test.rs index afaff14c5b..90bf89d168 100644 --- a/lib/segment/tests/integration/multivector_quantization_test.rs +++ b/lib/segment/tests/integration/multivector_quantization_test.rs @@ -273,6 +273,7 @@ fn test_multivector_quantization_hnsw( .into(), QuantizationVariant::Binary => BinaryQuantizationConfig { always_ram: Some(false), + encoding: None, } .into(), };