velesdb_core/quantization/
scalar.rs1use std::io;
8
9use super::codec_helpers::{serialize_with_header, validate_and_split_header};
10use super::QuantizationCodec;
11
12#[derive(Debug, Clone)]
22pub struct QuantizedVector {
23 pub data: Vec<u8>,
25 pub min: f32,
27 pub max: f32,
29}
30
31impl QuantizedVector {
32 #[must_use]
38 pub fn from_f32(vector: &[f32]) -> Self {
39 debug_assert!(!vector.is_empty(), "Cannot quantize empty vector");
41
42 let min = vector.iter().copied().fold(f32::INFINITY, f32::min);
43 let max = vector.iter().copied().fold(f32::NEG_INFINITY, f32::max);
44
45 let range = max - min;
46 let data = if range < f32::EPSILON {
47 vec![128u8; vector.len()]
49 } else {
50 let scale = 255.0 / range;
51 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
55 vector
56 .iter()
57 .map(|&v| {
58 let normalized = (v - min) * scale;
59 normalized.round().clamp(0.0, 255.0) as u8
60 })
61 .collect()
62 };
63
64 Self { data, min, max }
65 }
66
67 #[must_use]
71 pub fn to_f32(&self) -> Vec<f32> {
72 let range = self.max - self.min;
73 if range < f32::EPSILON {
74 vec![self.min; self.data.len()]
76 } else {
77 let scale = range / 255.0;
78 self.data
79 .iter()
80 .map(|&v| f32::from(v) * scale + self.min)
81 .collect()
82 }
83 }
84
85 #[must_use]
87 pub fn dimension(&self) -> usize {
88 self.data.len()
89 }
90
91 #[must_use]
93 pub fn memory_size(&self) -> usize {
94 self.data.len() + 8 }
96}
97
98const SQ8_HEADER_SIZE: usize = 8;
100
101impl QuantizationCodec for QuantizedVector {
102 fn to_bytes(&self) -> Vec<u8> {
103 let mut header = [0u8; SQ8_HEADER_SIZE];
104 header[..4].copy_from_slice(&self.min.to_le_bytes());
105 header[4..].copy_from_slice(&self.max.to_le_bytes());
106 serialize_with_header(&header, &self.data)
107 }
108
109 fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
110 let (header, payload) =
111 validate_and_split_header(bytes, SQ8_HEADER_SIZE, "QuantizedVector")?;
112
113 let min = f32::from_le_bytes([header[0], header[1], header[2], header[3]]);
114 let max = f32::from_le_bytes([header[4], header[5], header[6], header[7]]);
115 let data = payload.to_vec();
116
117 Ok(Self { data, min, max })
118 }
119}
120
121struct DequantParams {
130 scale: f32,
131 offset: f32,
132}
133
134fn dequant_params(quantized: &QuantizedVector) -> Option<DequantParams> {
139 let range = quantized.max - quantized.min;
140 if range < f32::EPSILON {
141 return None;
142 }
143 Some(DequantParams {
144 scale: range / 255.0,
145 offset: quantized.min,
146 })
147}
148
149#[must_use]
157pub fn dot_product_quantized(query: &[f32], quantized: &QuantizedVector) -> f32 {
158 debug_assert_eq!(
159 query.len(),
160 quantized.data.len(),
161 "Dimension mismatch in dot_product_quantized"
162 );
163
164 let Some(params) = dequant_params(quantized) else {
165 return query.iter().sum::<f32>() * quantized.min;
167 };
168
169 query
171 .iter()
172 .zip(quantized.data.iter())
173 .map(|(&q, &v)| q * (f32::from(v) * params.scale + params.offset))
174 .sum()
175}
176
177#[must_use]
179pub fn euclidean_squared_quantized(query: &[f32], quantized: &QuantizedVector) -> f32 {
180 debug_assert_eq!(
181 query.len(),
182 quantized.data.len(),
183 "Dimension mismatch in euclidean_squared_quantized"
184 );
185
186 let Some(params) = dequant_params(quantized) else {
187 let value = quantized.min;
188 return query.iter().map(|&q| (q - value).powi(2)).sum();
189 };
190
191 query
192 .iter()
193 .zip(quantized.data.iter())
194 .map(|(&q, &v)| {
195 let dequantized = f32::from(v) * params.scale + params.offset;
196 (q - dequantized).powi(2)
197 })
198 .sum()
199}
200
201#[must_use]
209pub fn cosine_similarity_quantized(query: &[f32], quantized: &QuantizedVector) -> f32 {
210 cosine_from_dot(dot_product_quantized(query, quantized), query, quantized)
211}
212
213fn cosine_from_dot(dot: f32, query: &[f32], quantized: &QuantizedVector) -> f32 {
219 use crate::simd_native;
220
221 let query_norm = simd_native::norm_native(query);
222
223 let quantized_norm = quantized_vector_norm(quantized);
225
226 if query_norm < f32::EPSILON || quantized_norm < f32::EPSILON {
227 return 0.0;
228 }
229
230 dot / (query_norm * quantized_norm)
231}
232
233#[inline]
238fn quantized_vector_norm(quantized: &QuantizedVector) -> f32 {
239 let Some(params) = dequant_params(quantized) else {
240 let value = quantized.min;
241 #[allow(clippy::cast_precision_loss)]
242 return value.abs() * (quantized.data.len() as f32).sqrt();
243 };
244
245 let len = quantized.data.len();
246 let chunks = len / 4;
247 let remainder = len % 4;
248
249 let mut sum0: f32 = 0.0;
250 let mut sum1: f32 = 0.0;
251 let mut sum2: f32 = 0.0;
252 let mut sum3: f32 = 0.0;
253
254 for i in 0..chunks {
255 let base = i * 4;
256 let d0 = f32::from(quantized.data[base]) * params.scale + params.offset;
257 let d1 = f32::from(quantized.data[base + 1]) * params.scale + params.offset;
258 let d2 = f32::from(quantized.data[base + 2]) * params.scale + params.offset;
259 let d3 = f32::from(quantized.data[base + 3]) * params.scale + params.offset;
260 sum0 += d0 * d0;
261 sum1 += d1 * d1;
262 sum2 += d2 * d2;
263 sum3 += d3 * d3;
264 }
265
266 let base = chunks * 4;
267 for i in 0..remainder {
268 let d = f32::from(quantized.data[base + i]) * params.scale + params.offset;
269 sum0 += d * d;
270 }
271
272 (sum0 + sum1 + sum2 + sum3).sqrt()
273}
274
275#[must_use]
287pub fn dot_product_quantized_simd(query: &[f32], quantized: &QuantizedVector) -> f32 {
288 debug_assert_eq!(
289 query.len(),
290 quantized.data.len(),
291 "Dimension mismatch in dot_product_quantized_simd"
292 );
293
294 let Some(params) = dequant_params(quantized) else {
295 return query.iter().sum::<f32>() * quantized.min;
296 };
297
298 dot_product_dequant_unrolled_8(query, &quantized.data, params.scale, params.offset)
299}
300
301#[inline]
303fn dot_product_dequant_unrolled_8(query: &[f32], data: &[u8], scale: f32, offset: f32) -> f32 {
304 let len = query.len();
305 let chunks = len / 8;
306 let remainder = len % 8;
307
308 let mut sum = 0.0f32;
309
310 for i in 0..chunks {
311 let base = i * 8;
312 for j in 0..8 {
313 let dequant = f32::from(data[base + j]) * scale + offset;
314 sum += query[base + j] * dequant;
315 }
316 }
317
318 let base = chunks * 8;
319 for i in 0..remainder {
320 let dequant = f32::from(data[base + i]) * scale + offset;
321 sum += query[base + i] * dequant;
322 }
323
324 sum
325}
326
327#[must_use]
329pub fn euclidean_squared_quantized_simd(query: &[f32], quantized: &QuantizedVector) -> f32 {
330 debug_assert_eq!(
331 query.len(),
332 quantized.data.len(),
333 "Dimension mismatch in euclidean_squared_quantized_simd"
334 );
335
336 let Some(params) = dequant_params(quantized) else {
337 let value = quantized.min;
338 return query.iter().map(|&q| (q - value).powi(2)).sum();
339 };
340
341 let len = query.len();
343 let chunks = len / 4;
344 let remainder = len % 4;
345 let mut sum = 0.0f32;
346
347 for i in 0..chunks {
348 let base = i * 4;
349 let d0 = f32::from(quantized.data[base]) * params.scale + params.offset;
350 let d1 = f32::from(quantized.data[base + 1]) * params.scale + params.offset;
351 let d2 = f32::from(quantized.data[base + 2]) * params.scale + params.offset;
352 let d3 = f32::from(quantized.data[base + 3]) * params.scale + params.offset;
353
354 let diff0 = query[base] - d0;
355 let diff1 = query[base + 1] - d1;
356 let diff2 = query[base + 2] - d2;
357 let diff3 = query[base + 3] - d3;
358
359 sum += diff0 * diff0 + diff1 * diff1 + diff2 * diff2 + diff3 * diff3;
360 }
361
362 let base = chunks * 4;
363 for i in 0..remainder {
364 let dequant = f32::from(quantized.data[base + i]) * params.scale + params.offset;
365 let diff = query[base + i] - dequant;
366 sum += diff * diff;
367 }
368
369 sum
370}
371
372#[must_use]
377pub fn cosine_similarity_quantized_simd(query: &[f32], quantized: &QuantizedVector) -> f32 {
378 cosine_from_dot(
379 dot_product_quantized_simd(query, quantized),
380 query,
381 quantized,
382 )
383}