1#[cfg(feature = "flatbuffers")]
7use thiserror::Error;
8
9use super::vectors::{DataMutRef, FullQueryMut, MinMaxCompensation, MinMaxIP, MinMaxL2Squared};
10use core::f32;
11
12use crate::{
13 AsFunctor, CompressInto,
14 algorithms::Transform,
15 alloc::{GlobalAllocator, ScopedAllocator},
16 bits::{Representation, Unsigned},
17 minmax::{MinMaxCosine, MinMaxCosineNormalized, vectors::FullQueryMeta},
18 num::Positive,
19 scalar::{InputContainsNaN, bit_scale},
20};
21#[cfg(feature = "flatbuffers")]
22use crate::{
23 algorithms::transforms::TransformError,
24 alloc::{Allocator, AllocatorError, Poly},
25 flatbuffers as fb,
26};
27
28#[cfg(all(not(test), feature = "flatbuffers"))]
30const DEFAULT_SERIALIZED_BYTES: usize = 1024;
31
32#[cfg(all(test, feature = "flatbuffers"))]
34const DEFAULT_SERIALIZED_BYTES: usize = 1;
35
36#[derive(Debug)]
68#[cfg_attr(test, derive(PartialEq))]
69pub struct MinMaxQuantizer {
70 transform: Transform<GlobalAllocator>,
74
75 grid_scale: Positive<f32>,
78}
79
80impl MinMaxQuantizer {
81 pub fn new(transform: Transform<GlobalAllocator>, grid_scale: Positive<f32>) -> Self {
83 Self {
84 transform,
85 grid_scale,
86 }
87 }
88
89 pub fn dim(&self) -> usize {
91 self.transform.input_dim()
92 }
93
94 pub fn output_dim(&self) -> usize {
99 self.transform.output_dim()
100 }
101
102 fn get_range<const NBITS: usize>(&self, vec: &[f32]) -> (f32, f32) {
118 let (min, max) = match NBITS {
119 1 => {
120 let (mut min, mut min_count) = (0.0f32, 0.0f32);
121 let (mut max, mut max_count) = (0.0f32, 0.0f32);
122
123 let mean = vec.iter().sum::<f32>() / (vec.len() as f32);
124
125 vec.iter().for_each(|x| {
126 let m = f32::from((*x < mean) as u8);
127 min += m * x;
128 min_count += m;
129 max += (1.0 - m) * x;
130 max_count += 1.0 - m;
131 });
132
133 ((min / min_count).min(mean), (max / max_count).max(mean))
134 }
135 _ => {
136 vec .iter()
138 .fold((f32::NAN, f32::NAN), |(cmin, cmax), &e| {
139 (cmin.min(e), cmax.max(e))
140 })
141 }
142 };
143
144 let width = (max - min) / 2.0;
145 let mid = min + width;
146
147 (
148 mid - width * self.grid_scale.into_inner(),
149 mid + width * self.grid_scale.into_inner(),
150 )
151 }
152
153 fn compress<const NBITS: usize, T>(
154 &self,
155 from: &[T],
156 mut into: DataMutRef<'_, NBITS>,
157 ) -> Result<L2Loss, InputContainsNaN>
158 where
159 T: Copy + Into<f32>,
160 Unsigned: Representation<NBITS>,
161 {
162 let mut into_vec = into.vector_mut();
163
164 assert_eq!(from.len(), self.dim());
165 assert_eq!(self.output_dim(), into_vec.len());
166
167 let domain = Unsigned::domain_const::<NBITS>();
168 let domain_min = *domain.start() as f32;
169 let domain_max = *domain.end() as f32;
170
171 let mut vec = vec![f32::default(); self.output_dim()];
172
173 #[allow(clippy::unwrap_used)]
175 self.transform
176 .transform_into(
177 &mut vec,
178 &from.iter().map(|&x| x.into()).collect::<Vec<f32>>(),
179 ScopedAllocator::global(),
180 )
181 .unwrap();
182
183 let (min, max) = self.get_range::<NBITS>(&vec);
184
185 let inverse_scale = (max - min).max(1e-8) / bit_scale::<NBITS>(); let mut norm_squared: f32 = 0.0;
187 let mut code_sum: f32 = 0.0;
188 let mut loss: f32 = 0.0;
189
190 let mut nan_check = false;
191
192 vec.iter().enumerate().for_each(|(i, &v)| {
193 nan_check |= v.is_nan();
194
195 let code = ((v - min) / inverse_scale)
196 .clamp(domain_min, domain_max)
197 .round();
198
199 let v_r = (code * inverse_scale) + min; norm_squared += v_r * v_r;
201 code_sum += code;
202 loss += (v_r - v).powi(2);
203
204 unsafe {
206 into_vec.set_unchecked(i, code as u8);
207 }
208 });
209
210 let meta = MinMaxCompensation {
211 dim: self.output_dim() as u32,
212 b: min,
213 a: inverse_scale,
214 n: inverse_scale * code_sum,
215 norm_squared,
216 };
217
218 into.set_meta(meta);
219
220 if nan_check {
221 Err(InputContainsNaN)
222 } else {
223 Ok(match Positive::new(loss) {
224 Ok(p) => L2Loss::Positive(p),
225 Err(_) => L2Loss::Zero,
226 })
227 }
228 }
229
230 #[cfg(feature = "flatbuffers")]
232 #[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
233 pub fn try_deserialize(data: &[u8]) -> Result<Self, DeserializationError> {
234 if !fb::minmax::quantizer_buffer_has_identifier(data) {
236 return Err(DeserializationError::InvalidIdentifier);
237 }
238
239 let root = fb::minmax::root_as_quantizer(data)?;
240
241 Ok(Self::new(
242 Transform::try_unpack(GlobalAllocator, root.transform())?,
243 Positive::new(root.grid_scale())
244 .map_err(|_| DeserializationError::GridScaleNotPositive)?,
245 ))
246 }
247
248 #[cfg(feature = "flatbuffers")]
249 pub fn serialize<A>(&self, allocator: A) -> Result<Poly<[u8], A>, AllocatorError>
250 where
251 A: Allocator + std::panic::UnwindSafe,
252 {
253 use flatbuffers::FlatBufferBuilder;
254
255 let mut buf = FlatBufferBuilder::new_in(Poly::broadcast(
256 0u8,
257 DEFAULT_SERIALIZED_BYTES,
258 allocator.clone(),
259 )?);
260
261 let transform = &self.transform;
262
263 let (root, mut buf) = match std::panic::catch_unwind(move || {
264 let offset = transform.pack(&mut buf);
265
266 let root = fb::minmax::Quantizer::create(
267 &mut buf,
268 &fb::minmax::QuantizerArgs {
269 transform: Some(offset),
270 grid_scale: self.grid_scale.into_inner(),
271 },
272 );
273 (root, buf)
274 }) {
275 Ok(ret) => ret,
276 Err(err) => match err.downcast_ref::<String>() {
277 Some(msg) => {
278 if msg.contains("AllocatorError") {
279 return Err(AllocatorError);
280 } else {
281 std::panic::resume_unwind(err);
282 }
283 }
284 None => std::panic::resume_unwind(err),
285 },
286 };
287
288 fb::minmax::finish_quantizer_buffer(&mut buf, root);
290 Poly::from_iter(buf.finished_data().iter().copied(), allocator)
291 }
292}
293
294#[cfg(feature = "flatbuffers")]
299#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
300#[derive(Debug, Clone, Error)]
301#[non_exhaustive]
302pub enum DeserializationError {
303 #[error("unhandled file identifier in flatbuffer")]
304 InvalidIdentifier,
305 #[error(transparent)]
306 InvalidFlatBuffer(#[from] flatbuffers::InvalidFlatbuffer),
307 #[error(transparent)]
308 AllocatorError(#[from] AllocatorError),
309 #[error(transparent)]
310 TransformError(#[from] TransformError),
311 #[error("grid-scale is missing or is not positive")]
312 GridScaleNotPositive,
313}
314
315#[derive(Clone, Copy, Debug)]
324pub enum L2Loss {
325 Zero,
326 Positive(Positive<f32>),
327}
328
329impl L2Loss {
330 pub fn as_f32(&self) -> f32 {
332 match self {
333 L2Loss::Zero => 0.0,
334 L2Loss::Positive(p) => p.into_inner(),
335 }
336 }
337}
338
339impl<const NBITS: usize, T> CompressInto<&[T], DataMutRef<'_, NBITS>> for MinMaxQuantizer
340where
341 T: Copy + Into<f32>,
342 Unsigned: Representation<NBITS>,
343{
344 type Error = InputContainsNaN;
345
346 type Output = L2Loss;
347
348 fn compress_into(&self, from: &[T], to: DataMutRef<'_, NBITS>) -> Result<L2Loss, Self::Error> {
365 self.compress::<NBITS, T>(from, to)
366 }
367}
368
369impl<'a, T> CompressInto<&[T], FullQueryMut<'a>> for MinMaxQuantizer
370where
371 T: Copy + Into<f32>,
372{
373 type Error = InputContainsNaN;
374
375 type Output = ();
376
377 fn compress_into(&self, from: &[T], mut to: FullQueryMut<'a>) -> Result<(), Self::Error> {
394 assert_eq!(from.len(), self.dim());
395 assert_eq!(self.output_dim(), to.len());
396
397 let from: Vec<f32> = from.iter().map(|&x| x.into()).collect();
399 if from.iter().any(|x| x.is_nan()) {
400 return Err(InputContainsNaN);
401 }
402
403 #[allow(clippy::unwrap_used)]
405 self.transform
406 .transform_into(to.vector_mut(), &from, ScopedAllocator::global())
407 .unwrap();
408
409 let norm_squared = to.vector().iter().map(|x| *x * *x).sum::<f32>();
410 let sum = to.vector().iter().sum::<f32>();
411
412 *to.meta_mut() = FullQueryMeta { norm_squared, sum };
413
414 Ok(())
415 }
416}
417
418macro_rules! impl_functor {
423 ($dist:ident) => {
424 impl AsFunctor<$dist> for MinMaxQuantizer {
425 fn as_functor(&self) -> $dist {
427 $dist
428 }
429 }
430 };
431}
432
433impl_functor!(MinMaxIP);
434impl_functor!(MinMaxL2Squared);
435impl_functor!(MinMaxCosine);
436impl_functor!(MinMaxCosineNormalized);
437
438#[cfg(test)]
442#[cfg(not(miri))]
443mod minmax_quantizer_tests {
444 use std::num::NonZeroUsize;
445
446 use diskann_utils::{Reborrow, ReborrowMut};
447 use diskann_vector::{PureDistanceFunction, distance::SquaredL2};
448 use rand::{
449 SeedableRng,
450 distr::{Distribution, Uniform},
451 rngs::StdRng,
452 };
453
454 use super::*;
455 use crate::{
456 algorithms::transforms::NullTransform,
457 alloc::GlobalAllocator,
458 minmax::vectors::{Data, DataRef, FullQuery, FullQueryMut},
459 };
460
461 fn reconstruct_minmax<const NBITS: usize>(v: DataRef<'_, NBITS>) -> Vec<f32>
462 where
463 Unsigned: Representation<NBITS>,
464 {
465 (0..v.len())
466 .map(|i| {
467 let m = v.meta();
468 v.vector().get(i).unwrap() as f32 * m.a + m.b
469 })
470 .collect()
471 }
472
473 fn test_quantizer_encoding_random<const NBITS: usize>(
474 dim: usize,
475 rng: &mut StdRng,
476 relative_err: f32,
477 scale: f32,
478 ) where
479 Unsigned: Representation<NBITS>,
480 MinMaxQuantizer: for<'a, 'b> CompressInto<&'a [f32], DataMutRef<'b, NBITS>, Output = L2Loss>
481 + for<'a, 'b> CompressInto<&'a [f32], FullQueryMut<'b>, Output = ()>,
482 {
483 let distribution = Uniform::new_inclusive::<f32, f32>(-1.0, 1.0).unwrap();
484
485 let quantizer = MinMaxQuantizer::new(
486 Transform::Null(NullTransform::new(NonZeroUsize::new(dim).unwrap())),
487 Positive::new(scale).unwrap(),
488 );
489
490 assert_eq!(quantizer.dim(), dim);
491
492 let vector: Vec<f32> = distribution.sample_iter(rng).take(dim).collect();
493
494 let mut encoded = Data::new_boxed(dim);
495 let loss = quantizer
496 .compress_into(&*vector, encoded.reborrow_mut())
497 .unwrap();
498
499 let reconstructed = reconstruct_minmax::<NBITS>(encoded.reborrow());
500 assert_eq!(reconstructed.len(), dim);
501
502 let reconstruction_error: f32 = SquaredL2::evaluate(&*vector, &*reconstructed);
503 let norm = vector.iter().map(|x| x * x).sum::<f32>();
504 assert!(
505 (reconstruction_error / norm) <= relative_err,
506 "Expected vector : {:?} to be reconstructed within error {} but instead got : {:?}, with error {} for dim : {}",
507 vector,
508 relative_err,
509 reconstructed,
510 reconstruction_error / norm,
511 dim,
512 );
513
514 assert!((loss.as_f32() - reconstruction_error) <= 1e-4);
515
516 let expected_code_sum = (0..dim)
517 .map(|i| encoded.vector().get(i).unwrap() as f32)
518 .sum::<f32>();
519 let code_sum = encoded.reborrow().meta().n / encoded.reborrow().meta().a;
520 assert!(
521 (code_sum - expected_code_sum).abs() <= 2e-5 * (dim as f32),
522 "Encoded vector with dim : {dim} is {:?}, got error : {} for vector : {:?}",
523 encoded.reborrow(),
524 (code_sum - expected_code_sum).abs(),
525 vector,
526 );
527 let recon_norm_sq = reconstructed.iter().map(|x| x * x).sum::<f32>();
528 assert!((encoded.reborrow().meta().norm_squared - recon_norm_sq).abs() <= 1e-3);
529
530 let mut f = FullQuery::new_in(dim, GlobalAllocator).unwrap();
532 quantizer
533 .compress_into(vector.as_slice(), f.reborrow_mut())
534 .unwrap();
535
536 f.vector()
537 .iter()
538 .enumerate()
539 .zip(vector.iter())
540 .for_each(|((i, x), y)| {
541 assert!(
542 (*x - *y).abs() < 1e-10,
543 "Full Query did not compress dimension {i} with value {} correctly, got {} instead.",
544 *y,
545 *x,
546 )
547 });
548
549 assert!(
550 (f.meta().norm_squared - norm).abs() < 1e-10,
551 "Full Query norm in meta should be {norm} but instead got {}",
552 f.meta().norm_squared
553 );
554
555 let sum = vector.iter().sum::<f32>();
556 assert!(
557 (f.meta().sum - sum) < 1e-10,
558 "Full Query norm in meta should be {sum} but instead got {}",
559 f.meta().sum
560 );
561 }
562
563 cfg_if::cfg_if! {
564 if #[cfg(miri)] {
565 const TRIALS: usize = 2;
569 } else {
570 const TRIALS: usize = 10;
571 }
572 }
573
574 macro_rules! test_minmax_quantizer_encoding {
575 ($name:ident, $dim:literal, $nbits:literal, $seed:literal, $err:expr) => {
576 #[test]
577 fn $name() {
578 let mut rng = StdRng::seed_from_u64($seed);
579 let scales = [1.0, 1.1, 0.9];
580 for (s, e) in scales.iter().zip($err) {
581 for d in 10..$dim {
582 for _ in 0..TRIALS {
583 test_quantizer_encoding_random::<$nbits>(d, &mut rng, e, *s);
584 }
585 }
586 }
587 }
588 };
589 }
590 test_minmax_quantizer_encoding!(
591 test_minmax_encoding_1bit,
592 100,
593 1,
594 0xa32d5658097a1c35,
595 vec![0.5, 0.5, 0.5]
596 );
597 test_minmax_quantizer_encoding!(
598 test_minmax_encoding_2bit,
599 100,
600 2,
601 0xf60c0c8d1aadc126,
602 vec![0.5, 0.5, 0.5]
603 );
604 test_minmax_quantizer_encoding!(
605 test_minmax_encoding_4bit,
606 100,
607 4,
608 0x09fa14c42a9d7d98,
609 vec![1.0e-2, 1.0e-2, 3.0e-2]
610 );
611 test_minmax_quantizer_encoding!(
612 test_minmax_encoding_8bit,
613 100,
614 8,
615 0xaedf3d2a223b7b77,
616 vec![2.0e-3, 2.0e-3, 7.0e-3]
617 );
618
619 macro_rules! expand_to_bitrates {
620 ($name:ident, $func:ident) => {
621 #[test]
622 fn $name() {
623 $func::<1>();
624 $func::<2>();
625 $func::<4>();
626 $func::<8>();
627 }
628 };
629 }
630
631 fn test_all_same_value_vector<const NBITS: usize>()
633 where
634 Unsigned: Representation<NBITS>,
635 MinMaxQuantizer:
636 for<'a, 'b> CompressInto<&'a [f32], DataMutRef<'b, NBITS>, Output = L2Loss>,
637 {
638 let dim = 30;
639 let quantizer = MinMaxQuantizer::new(
640 Transform::Null(NullTransform::new(NonZeroUsize::new(dim).unwrap())),
641 Positive::new(1.0).unwrap(),
642 );
643 let constant_value = 42.5f32;
644 let vector = vec![constant_value; dim];
645
646 let mut encoded = Data::new_boxed(dim);
647 let result = quantizer.compress_into(&vector, encoded.reborrow_mut());
648
649 assert!(
650 result.is_ok(),
651 "Constant-value vector should compress successfully"
652 );
653
654 assert!(result.unwrap().as_f32().abs() <= 1e-6);
655
656 let reconstructed = reconstruct_minmax(encoded.reborrow());
658 for &val in &reconstructed {
659 assert!(
660 (val - constant_value).abs() < 1e-3,
661 "Reconstructed value {} should be close to original {}. Compressed vector is {:?}",
662 val,
663 constant_value,
664 encoded.meta(),
665 );
666 }
667 }
668
669 fn test_two_distinct_values<const NBITS: usize>()
671 where
672 Unsigned: Representation<NBITS>,
673 MinMaxQuantizer:
674 for<'a, 'b> CompressInto<&'a [f32], DataMutRef<'b, NBITS>, Output = L2Loss>,
675 {
676 let dim = 20;
677 let quantizer = MinMaxQuantizer::new(
678 Transform::Null(NullTransform::new(NonZeroUsize::new(dim).unwrap())),
679 Positive::new(1.0).unwrap(),
680 );
681
682 let val1 = -10.0f32;
683 let val2 = 15.0f32;
684 let mut vector = vec![val1; dim];
685 for i in vector.iter_mut().skip(dim) {
687 *i = val2;
688 }
689
690 let mut encoded = Data::new_boxed(dim);
691 let result = quantizer.compress_into(&vector, encoded.reborrow_mut());
692
693 assert!(
694 result.is_ok(),
695 "Two-value vector should compress successfully"
696 );
697
698 assert!(result.unwrap().as_f32().abs() <= 1e-6);
699
700 let mut codes_used = std::collections::HashSet::new();
702 for i in 0..dim {
703 codes_used.insert(encoded.vector().get(i).unwrap());
704 }
705
706 if NBITS > 1 {
708 assert!(
709 codes_used.len() <= 2,
710 "Should use at most 2 distinct codes for 2-value input, but used: {:?}",
711 codes_used
712 );
713 }
714
715 let reconstructed = reconstruct_minmax(encoded.reborrow());
717 for ((i, val), v) in reconstructed.into_iter().enumerate().zip(&vector) {
718 assert!(
720 (val - v).abs() < 1e-4,
721 "Reconstructed value in dim : {i} is {val}, when it should be {v}."
722 );
723 }
724 }
725
726 fn test_nan_input_error<const NBITS: usize>()
729 where
730 Unsigned: Representation<NBITS>,
731 MinMaxQuantizer:
732 for<'a, 'b> CompressInto<&'a [f32], DataMutRef<'b, NBITS>, Output = L2Loss>,
733 {
734 let dim = 100;
735 let quantizer = MinMaxQuantizer::new(
736 Transform::Null(NullTransform::new(NonZeroUsize::new(dim).unwrap())),
737 Positive::new(1.0).unwrap(),
738 );
739
740 let mut vector_nan = vec![1.0f32; dim];
742 vector_nan[33] = f32::NAN;
743 let mut encoded = Data::new_boxed(dim);
744 let result = quantizer.compress_into(&vector_nan, encoded.reborrow_mut());
745 assert!(result.is_err(), "Vector with NaN should cause an error");
746
747 let meta = encoded.meta();
748 assert_eq!(meta.dim as usize, dim);
749 }
750
751 expand_to_bitrates!(all_same_values_vector, test_all_same_value_vector);
752 expand_to_bitrates!(two_distinct_values, test_two_distinct_values);
753 expand_to_bitrates!(nan_input_error, test_nan_input_error);
754
755 #[test]
757 #[should_panic(expected = "assertion `left == right` failed\n left: 15\n right: 10")]
758 fn test_dimension_mismatch_panic()
759 where
760 Unsigned: Representation<8>,
761 MinMaxQuantizer: for<'a, 'b> CompressInto<&'a [f32], DataMutRef<'b, 8>, Output = L2Loss>,
762 {
763 let expected_dim = 10;
764 let quantizer = MinMaxQuantizer::new(
765 Transform::Null(NullTransform::new(NonZeroUsize::new(expected_dim).unwrap())),
766 Positive::new(1.0).unwrap(),
767 );
768
769 let wrong_vector = vec![1.0f32; expected_dim + 5]; let mut encoded = Data::new_boxed(expected_dim);
772
773 let _ = quantizer.compress_into(&wrong_vector, encoded.reborrow_mut());
775 }
776
777 #[cfg(feature = "flatbuffers")]
778 mod serialization {
779 use std::sync::{
780 Arc,
781 atomic::{AtomicBool, Ordering},
782 };
783
784 use super::*;
785 use crate::{
786 algorithms::{TransformKind, transforms::TargetDim},
787 alloc::{AllocatorCore, AllocatorError},
788 };
789
790 fn make_hadamard_quantizer(dim: usize, grid_scale: f32, seed: u64) -> MinMaxQuantizer {
794 let mut rng = StdRng::seed_from_u64(seed);
795 let transform = Transform::new(
796 TransformKind::DoubleHadamard {
797 target_dim: TargetDim::Same,
798 },
799 NonZeroUsize::new(dim).unwrap(),
800 Some(&mut rng),
801 GlobalAllocator,
802 )
803 .unwrap();
804
805 MinMaxQuantizer::new(transform, Positive::new(grid_scale).unwrap())
806 }
807
808 fn test_roundtrip_inner<const NBITS: usize>(seed: u64)
809 where
810 Unsigned: Representation<NBITS>,
811 MinMaxQuantizer:
812 for<'a, 'b> CompressInto<&'a [f32], DataMutRef<'b, NBITS>, Output = L2Loss>,
813 {
814 let dim = 16;
815 let quantizer = make_hadamard_quantizer(dim, 0.9, seed);
816
817 let serialized = quantizer.serialize(GlobalAllocator).unwrap();
818 let deserialized = MinMaxQuantizer::try_deserialize(&serialized).unwrap();
819
820 assert_eq!(deserialized, quantizer);
821 }
822
823 macro_rules! roundtrip_test {
824 ($name:ident, $nbits:literal, $seed:literal) => {
825 #[test]
826 fn $name() {
827 test_roundtrip_inner::<$nbits>($seed);
828 }
829 };
830 }
831 roundtrip_test!(serialize_roundtrip_1bit, 1, 0x1b3f_8c21_6d90_a54e);
832 roundtrip_test!(serialize_roundtrip_2bit, 2, 0x77c2_e0a4_35bd_1f08);
833 roundtrip_test!(serialize_roundtrip_4bit, 4, 0x0a9d_4471_c8e2_9b6f);
834 roundtrip_test!(serialize_roundtrip_8bit, 8, 0xd4e1_5aa9_3f7c_2210);
835
836 #[test]
839 fn deserialize_rejects_invalid_identifier() {
840 assert!(matches!(
843 MinMaxQuantizer::try_deserialize(&[0u8; 32]),
844 Err(DeserializationError::InvalidIdentifier),
845 ));
846
847 let quantizer = make_hadamard_quantizer(16, 1.0, 0x51ed_9c33);
849 let mut serialized = quantizer.serialize(GlobalAllocator).unwrap().to_vec();
850 serialized[4] ^= 0xff;
852 assert!(matches!(
853 MinMaxQuantizer::try_deserialize(&serialized),
854 Err(DeserializationError::InvalidIdentifier),
855 ));
856 }
857
858 #[derive(Debug, Clone)]
861 struct FlakyAllocator {
862 have_allocated: Arc<AtomicBool>,
863 }
864
865 impl FlakyAllocator {
866 fn new(have_allocated: Arc<AtomicBool>) -> Self {
867 Self { have_allocated }
868 }
869 }
870
871 unsafe impl AllocatorCore for FlakyAllocator {
873 fn allocate(
874 &self,
875 layout: std::alloc::Layout,
876 ) -> Result<std::ptr::NonNull<[u8]>, AllocatorError> {
877 if self.have_allocated.swap(true, Ordering::Relaxed) {
878 Err(AllocatorError)
879 } else {
880 GlobalAllocator.allocate(layout)
881 }
882 }
883
884 unsafe fn deallocate(&self, ptr: std::ptr::NonNull<[u8]>, layout: std::alloc::Layout) {
885 unsafe { GlobalAllocator.deallocate(ptr, layout) }
887 }
888 }
889
890 #[test]
895 fn serialize_allocation_failure_does_not_panic() {
896 let quantizer = make_hadamard_quantizer(16, 1.0, 0x7a11_36bd);
897 let have_allocated = Arc::new(AtomicBool::new(false));
898 let _: AllocatorError = quantizer
899 .serialize(FlakyAllocator::new(have_allocated.clone()))
900 .unwrap_err();
901 assert!(have_allocated.load(Ordering::Relaxed));
902 }
903 }
904}