diskann_quantization/minmax/
recompress.rs1use super::vectors::{DataMutRef, DataRef, MinMaxCompensation};
7use crate::CompressInto;
8use crate::bits::{Representation, Unsigned};
9use crate::num::Positive;
10use crate::scalar::bit_scale;
11use thiserror::Error;
12
13#[derive(Debug, Clone, Copy)]
54pub struct Recompressor {
55 grid_scale: Positive<f32>,
56}
57
58impl Recompressor {
59 pub fn new(grid_scale: Positive<f32>) -> Self {
64 Self { grid_scale }
65 }
66
67 pub fn grid_scale(&self) -> Positive<f32> {
69 self.grid_scale
70 }
71}
72
73#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
75pub enum RecompressError {
76 #[error("dimension mismatch: source has {src} dimensions, destination has {dst}")]
78 DimensionMismatch {
79 src: usize,
81 dst: usize,
83 },
84}
85
86macro_rules! impl_recompress {
88 ($n:literal -> $m:literal) => {
89 impl<'a, 'b> CompressInto<DataRef<'a, $n>, DataMutRef<'b, $m>> for Recompressor
90 where
91 Unsigned: Representation<$n> + Representation<$m>,
92 {
93 type Error = RecompressError;
94 type Output = ();
95
96 fn compress_into(
97 &self,
98 from: DataRef<'a, $n>,
99 to: DataMutRef<'b, $m>,
100 ) -> Result<(), Self::Error> {
101 recompress_kernel::<$n, $m>(from, to, self.grid_scale.into_inner())
102 }
103 }
104 };
105}
106
107impl_recompress!(8 -> 4);
108impl_recompress!(8 -> 2);
109impl_recompress!(4 -> 2);
110
111#[inline(always)]
149fn recompress_kernel<const N: usize, const M: usize>(
150 from: DataRef<'_, N>,
151 mut to: DataMutRef<'_, M>,
152 grid_scale: f32,
153) -> Result<(), RecompressError>
154where
155 Unsigned: Representation<N> + Representation<M>,
156{
157 const { assert!(N > M, "source bit width must exceed target bits") };
158 const { assert!(M > 1, "target bit width must exceed 1") };
159
160 let dim = from.len();
162 if dim != to.vector().len() {
163 return Err(RecompressError::DimensionMismatch {
164 src: dim,
165 dst: to.vector().len(),
166 });
167 }
168
169 let src_meta = from.meta();
170 let src_a = src_meta.a;
171 let src_b = src_meta.b;
172
173 let scale_n = bit_scale::<N>();
174 let scale_m = bit_scale::<M>();
175
176 let offset = scale_n * (1.0 - grid_scale) * 0.5;
178 let code_scale = scale_m / (scale_n * grid_scale);
179
180 let new_a = src_a / code_scale;
182 let new_b = src_b + src_a * offset;
183
184 let from_vec = from.vector();
186 let mut to_vec = to.vector_mut();
187
188 let mut code_sum: f32 = 0.0;
189 let mut norm_squared: f32 = 0.0;
190
191 for i in 0..dim {
192 let old_code = unsafe { from_vec.get_unchecked(i) };
195 let old_code_f = old_code as f32;
196
197 let new_code = ((old_code_f - offset) * code_scale)
199 .round_ties_even()
200 .clamp(0.0, scale_m) as u8;
201
202 unsafe { to_vec.set_unchecked(i, new_code) };
205
206 let new_code_f = new_code as f32;
208 code_sum += new_code_f;
209
210 let v_m = new_code_f * new_a + new_b;
212
213 norm_squared += v_m * v_m;
214 }
215
216 to.set_meta(MinMaxCompensation {
218 dim: dim as u32,
219 b: new_b,
220 a: new_a,
221 n: new_a * code_sum,
222 norm_squared,
223 });
224
225 Ok(())
226}
227
228#[cfg(test)]
229mod recompress_tests {
230 use std::num::NonZeroUsize;
231
232 use diskann_utils::{Reborrow, ReborrowMut};
233 use rand::{
234 SeedableRng,
235 distr::{Distribution, Uniform},
236 rngs::StdRng,
237 };
238
239 use super::*;
240 use crate::{
241 algorithms::{Transform, transforms::NullTransform},
242 minmax::quantizer::MinMaxQuantizer,
243 minmax::vectors::Data,
244 num::Positive,
245 };
246
247 fn reconstruct<const NBITS: usize>(v: DataRef<'_, NBITS>) -> Vec<f32>
249 where
250 Unsigned: Representation<NBITS>,
251 {
252 let meta = v.meta();
253 (0..v.len())
254 .map(|i| v.vector().get(i).unwrap() as f32 * meta.a + meta.b)
255 .collect()
256 }
257
258 fn test_recompress_random<const N: usize, const M: usize>(
264 dim: usize,
265 grid_scale: f32,
266 rng: &mut StdRng,
267 ) where
268 Unsigned: Representation<N> + Representation<M>,
269 MinMaxQuantizer: for<'a, 'b> CompressInto<&'a [f32], DataMutRef<'b, N>>,
270 Recompressor: for<'a, 'b> CompressInto<DataRef<'a, N>, DataMutRef<'b, M>, Output = ()>,
271 {
272 let distribution = Uniform::new_inclusive::<f32, f32>(-1.0, 1.0).unwrap();
273 let quantizer = MinMaxQuantizer::new(
274 Transform::Null(NullTransform::new(NonZeroUsize::new(dim).unwrap())),
275 Positive::new(1.0).unwrap(),
276 );
277
278 let g = Positive::new(grid_scale).unwrap();
279
280 let recompressor = Recompressor::new(g);
281
282 assert_eq!(recompressor.grid_scale(), g);
283
284 let vector: Vec<f32> = distribution.sample_iter(rng).take(dim).collect();
286 let mut encoded_n = Data::<N>::new_boxed(dim);
287 quantizer
288 .compress_into(&*vector, encoded_n.reborrow_mut())
289 .unwrap();
290
291 let mut encoded_m = Data::<M>::new_boxed(dim);
293 recompressor
294 .compress_into(encoded_n.reborrow(), encoded_m.reborrow_mut())
295 .unwrap();
296
297 let scale_n = ((1u64 << N) - 1) as f32;
301 let scale_m = ((1u64 << M) - 1) as f32;
302 let offset = scale_n * (1.0 - grid_scale) * 0.5;
303 let code_scale = scale_m / (scale_n * grid_scale);
304 let expected_codes: Vec<u8> = (0..dim)
305 .map(|i| {
306 let c = encoded_n.vector().get(i).unwrap() as f32;
307 ((c - offset) * code_scale)
308 .round_ties_even()
309 .clamp(0.0, scale_m) as u8
310 })
311 .collect();
312
313 for (i, &expected) in expected_codes.iter().enumerate() {
315 let actual = encoded_m.vector().get(i).unwrap() as u8;
316 assert_eq!(
317 actual, expected,
318 "code mismatch at dim={dim}, g={grid_scale}, i={i}: expected {expected}, got {actual}"
319 );
320 }
321
322 let meta_m = encoded_m.meta();
324 assert_eq!(meta_m.dim as usize, dim, "Dimension should be preserved");
325
326 let actual_code_sum: f32 = (0..dim)
327 .map(|i| encoded_m.vector().get(i).unwrap() as f32)
328 .sum();
329 let computed_code_sum = meta_m.n / meta_m.a;
330 assert!(
331 (computed_code_sum - actual_code_sum).abs() < 1e-4,
332 "Code sum mismatch at g={grid_scale}: expected {actual_code_sum}, got {computed_code_sum}"
333 );
334
335 let reconstructed_m = reconstruct(encoded_m.reborrow());
336 let expected_norm_sq: f32 = reconstructed_m.iter().map(|x| x * x).sum();
337 assert!(
338 (meta_m.norm_squared - expected_norm_sq).abs() < 1e-4,
339 "norm_squared mismatch at g={grid_scale}: expected {expected_norm_sq}, got {}",
340 meta_m.norm_squared
341 );
342 }
343
344 cfg_if::cfg_if! {
345 if #[cfg(miri)] {
346 const TRIALS: usize = 2;
347 const MAX_DIM: usize = 20;
348 } else {
349 const TRIALS: usize = 10;
350 const MAX_DIM: usize = 100;
351 }
352 }
353
354 const GRID_SCALES: &[f32] = &[1.0, 0.8, 0.6, 1.2];
358
359 macro_rules! test_recompress_pair {
360 ($name:ident, $n:literal -> $m:literal, $seed:literal) => {
361 #[test]
362 fn $name() {
363 let mut rng = StdRng::seed_from_u64($seed);
364 for dim in 10..=MAX_DIM {
365 for _ in 0..TRIALS {
366 for &g in GRID_SCALES {
367 test_recompress_random::<$n, $m>(dim, g, &mut rng);
368 }
369 }
370 }
371 }
372 };
373 }
374
375 test_recompress_pair!(recompress_8_to_4, 8 -> 4, 0xabc123def456);
376 test_recompress_pair!(recompress_8_to_2, 8 -> 2, 0xdef456abc123);
377 test_recompress_pair!(recompress_4_to_2, 4 -> 2, 0x456def123abc);
378
379 #[test]
380 fn test_dimension_mismatch_error() {
381 let recompressor = Recompressor::new(Positive::<f32>::new(1.0).unwrap());
383
384 let mut src = Data::<8>::new_boxed(10);
385 src.set_meta(MinMaxCompensation {
386 dim: 10,
387 b: 0.0,
388 a: 1.0,
389 n: 0.0,
390 norm_squared: 0.0,
391 });
392
393 let mut dst = Data::<4>::new_boxed(15); let result: Result<(), RecompressError> =
396 recompressor.compress_into(src.reborrow(), dst.reborrow_mut());
397
398 assert_eq!(
399 result.unwrap_err(),
400 RecompressError::DimensionMismatch { src: 10, dst: 15 }
401 );
402 }
403
404 #[test]
405 fn test_constant_value_vector() {
406 let dim = 30;
407 let quantizer = MinMaxQuantizer::new(
408 Transform::Null(NullTransform::new(NonZeroUsize::new(dim).unwrap())),
409 Positive::new(1.0).unwrap(),
410 );
411
412 let recompressor = Recompressor::new(Positive::<f32>::new(1.0).unwrap());
414
415 let constant_value = 42.5f32;
416 let vector = vec![constant_value; dim];
417
418 let mut encoded_8 = Data::<8>::new_boxed(dim);
420 quantizer
421 .compress_into(&*vector, encoded_8.reborrow_mut())
422 .unwrap();
423
424 let mut encoded_4 = Data::<4>::new_boxed(dim);
426 recompressor
427 .compress_into(encoded_8.reborrow(), encoded_4.reborrow_mut())
428 .unwrap();
429
430 let first_code = encoded_4.vector().get(0).unwrap();
432 for i in 1..dim {
433 assert_eq!(
434 encoded_4.vector().get(i).unwrap(),
435 first_code,
436 "All codes should be identical for constant-value vector"
437 );
438 }
439
440 let reconstructed = reconstruct(encoded_4.reborrow());
442 for &val in &reconstructed {
443 assert!(
444 (val - constant_value).abs() < 1.0,
445 "Reconstructed value {} should be close to original {}",
446 val,
447 constant_value
448 );
449 }
450 }
451}