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
This commit is contained in:
Ivan Pleshkov
2025-06-19 18:21:00 +02:00
committed by GitHub
parent f79ad06ae9
commit fe5becdadd
19 changed files with 697 additions and 47 deletions

View File

@@ -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. |
<a name="qdrant-BinaryQuantizationEncoding"></a>
### BinaryQuantizationEncoding
| Name | Number | Description |
| ---- | ------ | ----------- |
| OneBit | 0 | |
| TwoBits | 1 | |
| OneAndHalfBits | 2 | |
<a name="qdrant-CollectionStatus"></a>
### CollectionStatus

View File

@@ -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": [

View File

@@ -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<ProductQuantization> for segment::types::ProductQuantization {
}
}
impl From<segment::types::BinaryQuantizationEncoding> 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<BinaryQuantizationEncoding> 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<segment::types::BinaryQuantization> 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<BinaryQuantization> for segment::types::BinaryQuantization {
type Error = Status;
fn try_from(value: BinaryQuantization) -> Result<Self, Self::Error> {
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),
},
})
}
}

View File

@@ -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 {

View File

@@ -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<bool>,
/// Binary quantization encoding method
#[prost(enumeration = "BinaryQuantizationEncoding", optional, tag = "2")]
pub encoding: ::core::option::Option<i32>,
}
#[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<Self> {
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,

View File

@@ -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();

View File

@@ -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<TBitsStoreType: BitsStoreType, TStorage: EncodedStorage> {
encoded_vectors: TStorage,
metadata: Metadata,
vector_stats: Option<VectorStats>,
bits_store_type: PhantomData<TBitsStoreType>,
}
#[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<TBitsStoreType: BitsStoreType> {
encoded_vector: Vec<TBitsStoreType>,
}
@@ -26,6 +42,9 @@ pub struct EncodedBinVector<TBitsStoreType: BitsStoreType> {
#[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::<Self>();
let bits_count = u8::BITS as usize * std::mem::size_of::<Self>();
let mut result = size / bits_count;
if size % bits_count != 0 {
result += 1;
@@ -175,16 +194,24 @@ impl<TBitsStoreType: BitsStoreType, TStorage: EncodedStorage>
orig_data: impl Iterator<Item = impl AsRef<[f32]> + 'a> + Clone,
mut storage_builder: impl EncodedStorageBuilder<Storage = TStorage>,
vector_parameters: &VectorParameters,
encoding: Encoding,
stopped: &AtomicBool,
) -> Result<Self, EncodingError> {
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<TBitsStoreType: BitsStoreType, TStorage: EncodedStorage>
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<TBitsStoreType> {
let mut encoded_vector =
vec![Default::default(); TBitsStoreType::get_storage_size(vector.len())];
fn encode_vector(
vector: &[f32],
vector_stats: &Option<VectorStats>,
encoding: Encoding,
) -> EncodedBinVector<TBitsStoreType> {
let encoded_vector_size =
Self::get_quantized_vector_size_from_params(vector.len(), encoding)
/ std::mem::size_of::<TBitsStoreType>();
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::<TBitsStoreType>();
let one = TBitsStoreType::one();
for (i, &v) in vector.iter().enumerate() {
@@ -212,17 +261,137 @@ impl<TBitsStoreType: BitsStoreType, TStorage: EncodedStorage>
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<VectorStats>,
) {
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::<TBitsStoreType>();
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<VectorStats>,
) {
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::<TBitsStoreType>();
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::<TBitsStoreType>()
}
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<TBitsStoreType: BitsStoreType, TStorage: EncodedStorage>
pub fn vectors_count(&self) -> usize {
self.metadata.vector_parameters.count
}
fn get_vector_stats_path_from_meta_path(meta_path: &Path) -> Option<PathBuf> {
let mut vector_stats_path = meta_path.parent()?.to_owned();
vector_stats_path.push("vector_stats.json");
Some(vector_stats_path)
}
}
impl<TBitsStoreType: BitsStoreType, TStorage: EncodedStorage> EncodedVectors
@@ -286,6 +461,21 @@ impl<TBitsStoreType: BitsStoreType, TStorage: EncodedStorage> 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<TBitsStoreType: BitsStoreType, TStorage: EncodedStorage> EncodedVectors
) -> std::io::Result<Self> {
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<TBitsStoreType: BitsStoreType, TStorage: EncodedStorage> EncodedVectors
fn encode_query(&self, query: &[f32]) -> EncodedBinVector<TBitsStoreType> {
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(

View File

@@ -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};

View File

@@ -0,0 +1,97 @@
use serde::{Deserialize, Serialize};
use crate::VectorParameters;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VectorStats {
pub elements_stats: Vec<VectorElementStats>,
}
#[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<Item = impl AsRef<[f32]> + '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
}
}

View File

@@ -101,6 +101,7 @@ mod tests {
vector_data.iter(),
Vec::<u8>::new(),
&vector_parameters,
quantization::encoded_vectors_binary::Encoding::OneBit,
&AtomicBool::new(false),
)
.unwrap();

View File

@@ -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;

View File

@@ -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();

View File

@@ -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<f32> {
(0..dim)
.map(|_| generate_number(rng) / (dim as f32).sqrt())
.collect()
}
fn get_top(scores: &[f32], count: usize, invert: bool) -> Vec<usize> {
let mut indices: Vec<usize> = (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::<u128>(0, false);
test_binary_dot_impl::<u128>(600, false);
test_binary_dot_impl::<u128>(601, false);
test_binary_dot_impl::<u8>(600, false);
}
#[test]
fn test_binary_dot_inverted() {
test_binary_dot_impl::<u128>(700, true);
test_binary_dot_impl::<u8>(700, true);
}
fn test_binary_dot_impl<TBitsStoreType: BitsStoreType>(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<f32>> = 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::<TBitsStoreType, _>::encode(
vector_data.iter(),
Vec::<u8>::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<f32> = generate_vector(vector_dim, &mut rng);
let orig_scores: Vec<f32> = 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<f32> = (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::<Vec<_>>();
// 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
);
}
}
}

View File

@@ -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),
},
});

View File

@@ -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<bool>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub encoding: Option<BinaryQuantizationEncoding>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, Validate, Clone, PartialEq, Eq, Hash)]

View File

@@ -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<QuantizedVectorStorage> {
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::<u128, QuantizedMmapStorage>::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::<u8>::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<QuantizedVectorStorage> {
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::<u8, QuantizedMmapStorage>::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::<u8>::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(

View File

@@ -93,6 +93,7 @@ fn product_x4() -> WithQuantization {
fn binary() -> WithQuantization {
let config = BinaryQuantizationConfig {
always_ram: Some(true),
encoding: None,
}
.into();

View File

@@ -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

View File

@@ -273,6 +273,7 @@ fn test_multivector_quantization_hnsw(
.into(),
QuantizationVariant::Binary => BinaryQuantizationConfig {
always_ram: Some(false),
encoding: None,
}
.into(),
};