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