Skip to main content

diskann_quantization/minmax/multi/
meta.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! Meta types and compression for MinMax quantized multi-vectors.
7
8use std::ptr::NonNull;
9
10use super::super::MinMaxQuantizer;
11use super::super::vectors::DataMutRef;
12use crate::CompressInto;
13use crate::bits::{Representation, Unsigned};
14use crate::minmax::{self, Data};
15use crate::multi_vector::matrix::{
16    Defaulted, NewMut, NewOwned, NewRef, Repr, ReprMut, ReprOwned, SliceError,
17};
18use crate::multi_vector::{LayoutError, Mat, MatMut, MatRef, Standard};
19use crate::scalar::InputContainsNaN;
20use crate::utils;
21
22////////////////
23// MinMaxMeta //
24////////////////
25
26/// Metadata for MinMax quantized multi-vectors.
27///
28/// Stores the intrinsic dimension (output dimension after transform) which is
29/// needed to interpret each row of the quantized data. The row stride in bytes
30/// is computed using [`minmax::Data::canonical_bytes`].
31#[derive(Debug, Clone, Copy)]
32pub struct MinMaxMeta<const NBITS: usize> {
33    nrows: usize,
34    intrinsic_dim: usize,
35}
36
37impl<const NBITS: usize> MinMaxMeta<NBITS>
38where
39    Unsigned: Representation<NBITS>,
40{
41    /// Creates new MinMax metadata with the given number of rows and intrinsic dimension.
42    pub fn new(nrows: usize, intrinsic_dim: usize) -> Self {
43        Self {
44            nrows,
45            intrinsic_dim,
46        }
47    }
48
49    /// Returns the `intrinsic_dim`
50    pub fn intrinsic_dim(&self) -> usize {
51        self.intrinsic_dim
52    }
53
54    /// Returns the number of bytes from a canonical repr of
55    /// a minmax quantized vector, see [minmax::Data::canonical_bytes]
56    pub fn ncols(&self) -> usize {
57        Data::<NBITS>::canonical_bytes(self.intrinsic_dim)
58    }
59
60    fn bytes(&self) -> usize {
61        std::mem::size_of::<u8>() * self.nrows() * self.ncols()
62    }
63}
64
65// SAFETY: The implementation correctly computes row offsets and constructs valid
66// DataRef types from the underlying byte buffer. The ncols() method
67// returns the canonical byte size for each row, and get_row properly
68// slices the buffer at the correct offsets.
69unsafe impl<const NBITS: usize> Repr for MinMaxMeta<NBITS>
70where
71    Unsigned: Representation<NBITS>,
72{
73    type Row<'a> = crate::minmax::DataRef<'a, NBITS>;
74
75    fn nrows(&self) -> usize {
76        self.nrows
77    }
78
79    /// Returns `Layout` for for allocating [`crate::minmax::Data`]
80    ///
81    /// # Safety
82    /// - [`crate::minmax::MinMaxCompensation`] does not require alignment
83    ///   since it is always accessed by copying bytes first before casting.
84    fn layout(&self) -> Result<std::alloc::Layout, LayoutError> {
85        Ok(std::alloc::Layout::array::<u8>(
86            self.nrows() * self.ncols(),
87        )?)
88    }
89
90    /// Returns an immutable reference to the i-th row.
91    ///
92    /// # Safety
93    /// - The original pointer must point to a valid allocation of at least
94    ///   `std::mem::size_of::<u8>() * self.nrows() * self.ncols()` bytes for lifetime `'a`.
95    /// - `i` must be less than `self.nrows`.
96    unsafe fn get_row<'a>(self, ptr: NonNull<u8>, i: usize) -> Self::Row<'a> {
97        debug_assert!(i < self.nrows);
98        let len = self.ncols();
99
100        // SAFETY: If the orginal pointer was initialized correctly, by the bounds check for `i`,
101        // `i * self.ncols() .. (i + 1) * self.ncols()` is a valid memory access.
102        // SAFETY: `ncols` is initialized using `minmax::Data::canonical_bytes` and `intrinsic_dim`,
103        unsafe {
104            let row_ptr = ptr.as_ptr().add(i * len);
105            let slice = std::slice::from_raw_parts(row_ptr, len);
106
107            minmax::DataRef::<'a, NBITS>::from_canonical_unchecked(slice, self.intrinsic_dim)
108        }
109    }
110}
111
112// SAFETY: The implementation correctly computes row offsets and constructs valid
113// DataMutRef types from the underlying byte buffer.
114unsafe impl<const NBITS: usize> ReprMut for MinMaxMeta<NBITS>
115where
116    Unsigned: Representation<NBITS>,
117{
118    type RowMut<'a> = crate::minmax::DataMutRef<'a, NBITS>;
119
120    /// Returns a mutable reference to the i-th row.
121    ///
122    /// # Safety
123    /// - The original pointer must point to a valid allocation of at least
124    ///   `std::mem::size_of::<u8>() * self.bytes()` bytes for lifetime `'a`.
125    /// - `i` must be less than `self.nrows`.
126    /// - Caller must make sure they have exclusive access to the row
127    unsafe fn get_row_mut<'a>(self, ptr: NonNull<u8>, i: usize) -> Self::RowMut<'a> {
128        debug_assert!(i < self.nrows);
129        let len = self.ncols();
130
131        // SAFETY: If the orginal pointer was initialized correctly, by the bounds check for `i`,
132        // `i * self.ncols() .. (i + 1) * self.ncols()` is a valid memory access.
133        // SAFETY: `ncols` is initialized using `minmax::Data::canonical_bytes` and `intrinsic_dim`,
134        // SAFETY: Caller has ensured we have exclusive access to the bytes in row `i`
135        unsafe {
136            let row_ptr = ptr.as_ptr().add(i * len);
137            let slice = std::slice::from_raw_parts_mut(row_ptr, len);
138
139            minmax::DataMutRef::<'a, NBITS>::from_canonical_front_mut_unchecked(
140                slice,
141                self.intrinsic_dim,
142            )
143        }
144    }
145}
146
147// SAFETY: The drop implementation correctly reconstructs a Box from the raw pointer
148// using the same size that was used for allocation (self.bytes()), allowing Box
149// to properly deallocate the memory.
150unsafe impl<const NBITS: usize> ReprOwned for MinMaxMeta<NBITS>
151where
152    Unsigned: Representation<NBITS>,
153{
154    /// Deallocates the memory pointed to by `ptr`.
155    ///
156    /// # Safety:
157    /// - The caller guarantees that `ptr` was allocated with the correct layout.
158    unsafe fn drop(self, ptr: NonNull<u8>) {
159        let slice_ptr = std::ptr::slice_from_raw_parts_mut(ptr.as_ptr(), self.bytes());
160
161        // SAFETY: inherited from caller. All implementations that create a new owned
162        // `Mat<Self>` are compatible with `Box` deallocation.
163        let _ = unsafe { Box::from_raw(slice_ptr) };
164    }
165}
166
167// SAFETY: `ptr` points to a properly sized slice that is compatible with the drop
168// logic in `Self as ReprOwned`. Box guarantees that the initial construction
169// will be non-null.
170unsafe impl<const NBITS: usize> NewOwned<Defaulted> for MinMaxMeta<NBITS>
171where
172    Unsigned: Representation<NBITS>,
173{
174    type Error = crate::error::Infallible;
175    fn new_owned(self, _: Defaulted) -> Result<Mat<Self>, Self::Error> {
176        let b: Box<[u8]> = (0..self.bytes()).map(|_| u8::default()).collect();
177
178        let ptr = utils::box_into_nonnull(b).cast::<u8>();
179
180        // SAFETY: `ptr` points to a properly sized slice that is compatible with the drop
181        // logic in `Self as ReprOwned`.
182        let mat = unsafe { Mat::from_raw_parts(self, ptr) };
183        Ok(mat)
184    }
185}
186
187// SAFETY: This checks that the slice has the correct length, which is all that is
188// required for [`Repr`] in this case.
189unsafe impl<const NBITS: usize> NewRef<u8> for MinMaxMeta<NBITS>
190where
191    Unsigned: Representation<NBITS>,
192{
193    type Error = SliceError;
194
195    fn new_ref(self, slice: &[u8]) -> Result<MatRef<'_, Self>, Self::Error> {
196        let expected = self.bytes();
197        if slice.len() != expected {
198            return Err(SliceError::LengthMismatch {
199                expected,
200                found: slice.len(),
201            });
202        }
203
204        // SAFETY: We've verified that the slice has the correct length.
205        Ok(unsafe { MatRef::from_raw_parts(self, utils::as_nonnull(slice).cast::<u8>()) })
206    }
207}
208
209// SAFETY: This checks that the slice has the correct length, which is all that is
210// required for [`ReprMut`] in this case since [`minmax::Data`] has no alignment requirement.
211unsafe impl<const NBITS: usize> NewMut<u8> for MinMaxMeta<NBITS>
212where
213    Unsigned: Representation<NBITS>,
214{
215    type Error = SliceError;
216
217    fn new_mut(self, slice: &mut [u8]) -> Result<MatMut<'_, Self>, Self::Error> {
218        let expected = self.bytes();
219        if slice.len() != expected {
220            return Err(SliceError::LengthMismatch {
221                expected,
222                found: slice.len(),
223            });
224        }
225
226        // SAFETY: We've verified that the slice has the correct length.
227        Ok(unsafe { MatMut::from_raw_parts(self, utils::as_nonnull_mut(slice).cast::<u8>()) })
228    }
229}
230
231//////////////////
232// CompressInto //
233//////////////////
234
235impl<'a, 'b, const NBITS: usize, T>
236    CompressInto<MatRef<'a, Standard<T>>, MatMut<'b, MinMaxMeta<NBITS>>> for MinMaxQuantizer
237where
238    T: Copy + Into<f32>,
239    Unsigned: Representation<NBITS>,
240{
241    type Error = InputContainsNaN;
242
243    type Output = ();
244
245    /// Compress a multi-vector of full-precision vectors into a multi-vector of MinMax quantized vectors.
246    ///
247    /// This method iterates row by row over the input matrix, quantizing each full-precision
248    /// vector into the corresponding row of the output MinMax matrix.
249    ///
250    /// # Error
251    ///
252    /// Returns [`InputContainsNaN`] error if any input vector contains `NaN`.
253    ///
254    /// # Panics
255    ///
256    /// Panics if:
257    /// * `from.num_vectors() != to.num_vectors()`: The input and output must have the same number of vectors.
258    /// * `from.vector_dim() != self.dim()`: Each input vector must have the same dimensionality as the quantizer.
259    /// * The output intrinsic dimension doesn't match `self.output_dim()`.
260    fn compress_into(
261        &self,
262        from: MatRef<'a, Standard<T>>,
263        mut to: MatMut<'b, MinMaxMeta<NBITS>>,
264    ) -> Result<(), Self::Error> {
265        assert_eq!(
266            from.num_vectors(),
267            to.num_vectors(),
268            "input and output must have the same number of vectors: {} != {}",
269            from.num_vectors(),
270            to.num_vectors()
271        );
272        assert_eq!(
273            from.vector_dim(),
274            self.dim(),
275            "input vectors must match quantizer dimension: {} != {}",
276            from.vector_dim(),
277            self.dim()
278        );
279        assert_eq!(
280            to.repr().intrinsic_dim(),
281            self.output_dim(),
282            "output intrinsic dimension must match quantizer output dimension: {} != {}",
283            to.repr().intrinsic_dim(),
284            self.output_dim()
285        );
286
287        for (from_row, to_row) in from.rows().zip(to.rows_mut()) {
288            // Use the single-vector `CompressInto` implementation
289            let _ = <MinMaxQuantizer as CompressInto<&[T], DataMutRef<'_, NBITS>>>::compress_into(
290                self, from_row, to_row,
291            )?;
292        }
293
294        Ok(())
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301    use crate::algorithms::Transform;
302    use crate::algorithms::transforms::NullTransform;
303    use crate::minmax::vectors::DataRef;
304    use crate::num::Positive;
305    use diskann_utils::{Reborrow, ReborrowMut};
306    use std::num::NonZeroUsize;
307
308    /// Test dimensions for comprehensive coverage.
309    const TEST_DIMS: &[usize] = &[1, 2, 3, 4, 7, 8, 16, 31, 32, 64];
310    /// Test vector counts for multi-vector matrices.
311    const TEST_NVECS: &[usize] = &[1, 2, 3, 5, 10];
312
313    /// Macro to generate a single test that runs a generic function for all bitrates (1, 2, 4, 8).
314    macro_rules! expand_to_bitrates {
315        ($name:ident, $func:ident) => {
316            #[test]
317            fn $name() {
318                $func::<1>();
319                $func::<2>();
320                $func::<4>();
321                $func::<8>();
322            }
323        };
324    }
325
326    // ==================
327    // Helper Functions
328    // ==================
329
330    /// Creates a quantizer for testing with the given dimension.
331    fn make_quantizer(dim: usize) -> MinMaxQuantizer {
332        MinMaxQuantizer::new(
333            Transform::Null(NullTransform::new(NonZeroUsize::new(dim).unwrap())),
334            Positive::new(1.0).unwrap(),
335        )
336    }
337
338    /// Generates deterministic test data for a multi-vector.
339    fn generate_test_data(num_vectors: usize, dim: usize) -> Vec<f32> {
340        (0..num_vectors * dim)
341            .map(|i| {
342                let row = i / dim;
343                let col = i % dim;
344                // Vary values to exercise quantization: use sin/cos patterns
345                ((row as f32 + 1.0) * (col as f32 + 0.5)).sin() * 10.0 + (row as f32)
346            })
347            .collect()
348    }
349
350    /// Compresses a single vector using the single-vector CompressInto and returns the bytes.
351    fn compress_single_vector<const NBITS: usize>(
352        quantizer: &MinMaxQuantizer,
353        input: &[f32],
354        dim: usize,
355    ) -> Vec<u8>
356    where
357        Unsigned: Representation<NBITS>,
358    {
359        let row_bytes = Data::<NBITS>::canonical_bytes(dim);
360        let mut output = vec![0u8; row_bytes];
361        let output_ref =
362            crate::minmax::DataMutRef::<NBITS>::from_canonical_front_mut(&mut output, dim).unwrap();
363        let _ = <MinMaxQuantizer as CompressInto<&[f32], DataMutRef<'_, NBITS>>>::compress_into(
364            quantizer, input, output_ref,
365        )
366        .expect("single-vector compression should succeed");
367        output
368    }
369
370    // =============================
371    // Construction
372    // =============================
373
374    mod construction {
375        use super::*;
376
377        /// Tests NewOwned (Mat::new with Defaulted) for various dimensions and sizes.
378        fn test_new_owned<const NBITS: usize>()
379        where
380            Unsigned: Representation<NBITS>,
381        {
382            for &dim in TEST_DIMS {
383                for &num_vectors in TEST_NVECS {
384                    let meta = MinMaxMeta::<NBITS>::new(num_vectors, dim);
385                    let mat: Mat<MinMaxMeta<NBITS>> =
386                        Mat::new(meta, Defaulted).expect("NewOwned should succeed");
387
388                    // Verify num_vectors and intrinsic_dim
389                    assert_eq!(mat.num_vectors(), num_vectors);
390                    assert_eq!(mat.repr().intrinsic_dim(), dim);
391
392                    // Verify Repr::nrows() and Repr::ncols()
393                    let expected_row_bytes = Data::<NBITS>::canonical_bytes(dim);
394                    assert_eq!(mat.repr().nrows(), num_vectors);
395                    assert_eq!(mat.repr().ncols(), expected_row_bytes);
396
397                    // Verify Repr::layout() returns correct size (nrows * ncols bytes)
398                    let expected_bytes = expected_row_bytes * num_vectors;
399                    let layout = mat.repr().layout().expect("layout should succeed");
400                    assert_eq!(layout.size(), expected_bytes);
401
402                    // Verify we can access all rows
403                    for i in 0..num_vectors {
404                        let row = mat.get_row(i);
405                        assert!(row.is_some(), "row {i} should exist");
406                        assert_eq!(row.unwrap().len(), dim);
407                    }
408
409                    // Out of bounds access should return None
410                    assert!(mat.get_row(num_vectors).is_none());
411                }
412            }
413        }
414
415        expand_to_bitrates!(new_owned, test_new_owned);
416
417        /// Tests NewRef (MatRef::new) for valid slices.
418        fn test_new_ref<const NBITS: usize>()
419        where
420            Unsigned: Representation<NBITS>,
421        {
422            for &dim in TEST_DIMS {
423                for &num_vectors in TEST_NVECS {
424                    let meta = MinMaxMeta::<NBITS>::new(num_vectors, dim);
425                    let expected_row_bytes = Data::<NBITS>::canonical_bytes(dim);
426                    let expected_bytes = expected_row_bytes * num_vectors;
427
428                    // Valid slice
429                    let data = vec![0u8; expected_bytes];
430                    let mat_ref = MatRef::new(meta, &data);
431                    assert!(mat_ref.is_ok(), "NewRef should succeed for correct size");
432                    let mat_ref = mat_ref.unwrap();
433
434                    // Verify num_vectors and intrinsic_dim
435                    assert_eq!(mat_ref.num_vectors(), num_vectors);
436                    assert_eq!(mat_ref.repr().intrinsic_dim(), dim);
437
438                    // Verify Repr::nrows() and Repr::ncols()
439                    assert_eq!(mat_ref.repr().nrows(), num_vectors);
440                    assert_eq!(mat_ref.repr().ncols(), expected_row_bytes);
441
442                    // Verify Repr::layout() returns correct size
443                    let layout = mat_ref.repr().layout().expect("layout should succeed");
444                    assert_eq!(layout.size(), expected_bytes);
445                }
446            }
447        }
448
449        expand_to_bitrates!(new_ref, test_new_ref);
450
451        /// Tests NewMut (MatMut::new) for valid slices.
452        fn test_new_mut<const NBITS: usize>()
453        where
454            Unsigned: Representation<NBITS>,
455        {
456            for &dim in TEST_DIMS {
457                for &num_vectors in TEST_NVECS {
458                    let meta = MinMaxMeta::<NBITS>::new(num_vectors, dim);
459                    let expected_row_bytes = Data::<NBITS>::canonical_bytes(dim);
460                    let expected_bytes = expected_row_bytes * num_vectors;
461
462                    let mut data = vec![0u8; expected_bytes];
463                    let mat_mut = MatMut::new(meta, &mut data);
464                    assert!(mat_mut.is_ok(), "NewMut should succeed for correct size");
465                    let mat_mut = mat_mut.unwrap();
466
467                    // Verify num_vectors and intrinsic_dim
468                    assert_eq!(mat_mut.num_vectors(), num_vectors);
469                    assert_eq!(mat_mut.repr().intrinsic_dim(), dim);
470
471                    // Verify Repr::nrows() and Repr::ncols()
472                    assert_eq!(mat_mut.repr().nrows(), num_vectors);
473                    assert_eq!(mat_mut.repr().ncols(), expected_row_bytes);
474
475                    // Verify Repr::layout() returns correct size
476                    let layout = mat_mut.repr().layout().expect("layout should succeed");
477                    assert_eq!(layout.size(), expected_bytes);
478                }
479            }
480        }
481
482        expand_to_bitrates!(new_mut, test_new_mut);
483
484        #[test]
485        fn slice_length_mismatch_errors() {
486            let dim = 4;
487            let num_vectors = 2;
488            let meta = MinMaxMeta::<8>::new(num_vectors, dim);
489            let expected_bytes = DataRef::<8>::canonical_bytes(dim) * num_vectors;
490
491            // Too short
492            let short_data = vec![0u8; expected_bytes - 1];
493            let result = MatRef::new(meta, &short_data);
494            assert!(
495                matches!(result, Err(SliceError::LengthMismatch { .. })),
496                "should fail for too-short slice"
497            );
498
499            // Too long
500            let long_data = vec![0u8; expected_bytes + 1];
501            let result = MatRef::new(meta, &long_data);
502            assert!(
503                matches!(result, Err(SliceError::LengthMismatch { .. })),
504                "should fail for too-long slice"
505            );
506
507            // Mutable version - too short
508            let mut short_mut = vec![0u8; expected_bytes - 1];
509            let result = MatMut::new(meta, &mut short_mut);
510            assert!(
511                matches!(result, Err(SliceError::LengthMismatch { .. })),
512                "MatMut should fail for too-short slice"
513            );
514        }
515    }
516
517    // ===========================
518    // CompressInto Multi-Vector
519    // ===========================
520
521    mod compress_into {
522        use super::*;
523
524        /// Tests that multi-vector CompressInto produces identical results to
525        /// single-vector CompressInto applied row-by-row.
526        fn test_compress_matches_single<const NBITS: usize>()
527        where
528            Unsigned: Representation<NBITS>,
529        {
530            for &dim in TEST_DIMS {
531                for &num_vectors in TEST_NVECS {
532                    let quantizer = make_quantizer(dim);
533                    let input_data = generate_test_data(num_vectors, dim);
534
535                    // Multi-vector compression
536                    let input_view =
537                        MatRef::new(Standard::new(num_vectors, dim).unwrap(), &input_data)
538                            .expect("input view creation");
539
540                    let mut multi_mat: Mat<MinMaxMeta<NBITS>> =
541                        Mat::new(MinMaxMeta::new(num_vectors, dim), Defaulted)
542                            .expect("output mat creation");
543
544                    quantizer
545                        .compress_into(input_view, multi_mat.reborrow_mut())
546                        .expect("multi-vector compression");
547
548                    // Compare each row with single-vector compression
549                    for i in 0..num_vectors {
550                        let row_input = &input_data[i * dim..(i + 1) * dim];
551                        let expected_bytes =
552                            compress_single_vector::<NBITS>(&quantizer, row_input, dim);
553
554                        let actual_row = multi_mat.get_row(i).expect("row should exist");
555
556                        // Compare metadata
557                        // SAFETY: expected_bytes was produced by compress_single_vector with correct dim
558                        let expected_ref = unsafe {
559                            DataRef::<NBITS>::from_canonical_unchecked(&expected_bytes, dim)
560                        };
561                        assert_eq!(
562                            actual_row.meta(),
563                            expected_ref.meta(),
564                            "metadata mismatch at row {i}, dim={dim}, num_vectors={num_vectors}, NBITS={NBITS}"
565                        );
566
567                        // Compare quantized values
568                        for j in 0..dim {
569                            assert_eq!(
570                                actual_row.vector().get(j).unwrap(),
571                                expected_ref.vector().get(j).unwrap(),
572                                "quantized value mismatch at row {i}, col {j}"
573                            );
574                        }
575                    }
576                }
577            }
578        }
579
580        expand_to_bitrates!(compress_matches_single, test_compress_matches_single);
581
582        /// Tests row iteration after compression.
583        fn test_row_iteration<const NBITS: usize>()
584        where
585            Unsigned: Representation<NBITS>,
586        {
587            let dim = 8;
588            let num_vectors = 5;
589            let quantizer = make_quantizer(dim);
590            let input_data = generate_test_data(num_vectors, dim);
591
592            let input_view = MatRef::new(Standard::new(num_vectors, dim).unwrap(), &input_data)
593                .expect("input view");
594
595            let mut mat: Mat<MinMaxMeta<NBITS>> =
596                Mat::new(MinMaxMeta::new(num_vectors, dim), Defaulted).expect("mat creation");
597
598            quantizer
599                .compress_into(input_view, mat.reborrow_mut())
600                .expect("compression");
601
602            // Test rows() iterator using reborrow
603            let view = mat.reborrow();
604            let mut count = 0;
605            for row in view.rows() {
606                assert_eq!(row.len(), dim, "row should have correct dimension");
607                count += 1;
608            }
609            assert_eq!(count, num_vectors);
610        }
611
612        expand_to_bitrates!(row_iteration, test_row_iteration);
613    }
614
615    // ===========================
616    // Error Cases
617    // ===========================
618
619    mod error_cases {
620        use super::*;
621
622        #[test]
623        #[should_panic(expected = "input and output must have the same number of vectors")]
624        fn compress_into_vector_count_mismatch() {
625            const NBITS: usize = 8;
626            let dim = 4;
627            let quantizer = make_quantizer(dim);
628
629            // Input has 3 vectors
630            let input_data = generate_test_data(3, dim);
631            let input_view =
632                MatRef::new(Standard::new(3, dim).unwrap(), &input_data).expect("input view");
633
634            // Output has 2 vectors (mismatch)
635            let mut mat: Mat<MinMaxMeta<NBITS>> =
636                Mat::new(MinMaxMeta::new(2, dim), Defaulted).expect("mat creation");
637
638            let _ = quantizer.compress_into(input_view, mat.reborrow_mut());
639        }
640
641        #[test]
642        #[should_panic(expected = "input vectors must match quantizer dimension")]
643        fn compress_into_input_dim_mismatch() {
644            const NBITS: usize = 8;
645            let quantizer = make_quantizer(4); // quantizer expects dim=4
646
647            // Input has dim=8 (mismatch)
648            let input_data = generate_test_data(2, 8);
649            let input_view =
650                MatRef::new(Standard::new(2, 8).unwrap(), &input_data).expect("input view");
651
652            // Output correctly has dim=4
653            let mut mat: Mat<MinMaxMeta<NBITS>> =
654                Mat::new(MinMaxMeta::new(2, 4), Defaulted).expect("mat creation");
655
656            let _ = quantizer.compress_into(input_view, mat.reborrow_mut());
657        }
658
659        #[test]
660        #[should_panic(
661            expected = "output intrinsic dimension must match quantizer output dimension"
662        )]
663        fn compress_into_output_dim_mismatch() {
664            const NBITS: usize = 8;
665            let quantizer = make_quantizer(4);
666
667            // Input correctly has dim=4
668            let input_data = generate_test_data(2, 4);
669            let input_view =
670                MatRef::new(Standard::new(2, 4).unwrap(), &input_data).expect("input view");
671
672            // Output has intrinsic_dim=8 (mismatch)
673            let row_bytes = Data::<NBITS>::canonical_bytes(8);
674            let mut output_data = vec![0u8; 2 * row_bytes];
675            let output_view =
676                MatMut::new(MinMaxMeta::<NBITS>::new(2, 8), &mut output_data).expect("output view");
677
678            let _ = quantizer.compress_into(input_view, output_view);
679        }
680    }
681}