Skip to main content

diskann_quantization/
views.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6use std::num::NonZeroUsize;
7
8use diskann_utils::views::{DenseData, MutDenseData};
9use std::ops::{Index, IndexMut};
10use thiserror::Error;
11
12//////////////////
13// ChunkOffsets //
14//////////////////
15
16/// A wrapper class for PQ chunk offsets.
17///
18/// Upon construction, this class guarantees that the underlying chunk offset plan is valid.
19/// A valid PQ chunk offset plan records the starting offsets of each chunk such that chunk
20/// `i` of a slice `x` can be accessed using `x[offsets[i]..offsets[i+1]]`.
21///
22/// In particular, a valid PQ chunk offset plan has the following properties:
23///
24/// * It has a length of at least 2.
25/// * Its first entry is 0.
26/// * For `i` in `0..offsets.len()`, `offsets[i] < offsets[i+1]`.
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct ChunkOffsetsBase<T>
29where
30    T: DenseData<Elem = usize>,
31{
32    // Pre-compute the associated dimension for better locality.
33    //
34    // We could extract this as `offsets.last().unwrap() - 1`, but hoisting it out into
35    // the struct means it is readily available when needed.
36    dim: NonZeroUsize,
37    // Chunk Offsets
38    offsets: T,
39}
40
41#[derive(Error, Debug)]
42#[non_exhaustive]
43pub enum ChunkOffsetError {
44    #[error("offsets must have a length of at least 2, found {0}")]
45    LengthNotAtLeastTwo(usize),
46    #[error("offsets must begin at 0, not {0}")]
47    DoesNotBeginWithZero(usize),
48    #[error(
49        "offsets must be strictly increasing, \
50         instead entry {start_val} at position {start} is followed by {next_val}"
51    )]
52    NonMonotonic {
53        start_val: usize,
54        start: usize,
55        next_val: usize,
56    },
57}
58
59/// Error returned by [`ChunkOffsets::partition`].
60#[derive(Error, Debug)]
61#[error("num_chunks {num_chunks} must not exceed dim {dim}")]
62pub struct PartitionError {
63    pub num_chunks: usize,
64    pub dim: usize,
65}
66
67/// Error returned by [`ChunkOffsetsView::partition_into`].
68#[derive(Error, Debug)]
69#[non_exhaustive]
70pub enum PartitionIntoError {
71    #[error("scratch must have a length of at least 2, found {0}")]
72    ScratchTooSmall(usize),
73    #[error(transparent)]
74    PartitionError(#[from] PartitionError),
75}
76
77impl<T> ChunkOffsetsBase<T>
78where
79    T: DenseData<Elem = usize>,
80{
81    /// Construct a new `ChunkOffset` from a raw slice.
82    ///
83    /// Returns an error if:
84    /// * The length of `offsets` is less than 2.
85    /// * The entries in `offsets` are not strictly increasing.
86    pub fn new(offsets: T) -> Result<Self, ChunkOffsetError> {
87        let slice = offsets.as_slice();
88
89        // Validate that the length is correct.
90        let len = slice.len();
91        if len < 2 {
92            return Err(ChunkOffsetError::LengthNotAtLeastTwo(len));
93        }
94
95        // Check that we don't start at zero.
96        let start = slice[0];
97        if start != 0 {
98            return Err(ChunkOffsetError::DoesNotBeginWithZero(start));
99        }
100
101        // What follows is a convoluted dance to safely get the source dimension as a
102        // `NonZeroUsize` while validating the monotonicity of the offsets in the provided
103        // slice.
104        //
105        // We can seed the `NonZeroUsize` with the knowledge that we've already checked
106        // that the length of the `slice` vector is at least two.
107        let mut last: NonZeroUsize = match NonZeroUsize::new(slice[1]) {
108            Some(x) => Ok(x),
109            None => Err(ChunkOffsetError::NonMonotonic {
110                start_val: start,
111                start: 0,
112                next_val: 0,
113            }),
114        }?;
115
116        // Now that we've successfully initialized a `NonZeroUsize` - we can use it going
117        // forward.
118        //
119        // Validate that `offsets` is monotonic.
120        for i in 2..slice.len() {
121            let start_val = slice[i - 1];
122            let next_val = NonZeroUsize::new(slice[i]);
123            last = match next_val {
124                Some(next_val) => {
125                    if start_val >= next_val.get() {
126                        Err(ChunkOffsetError::NonMonotonic {
127                            start_val,
128                            start: i - 1,
129                            next_val: next_val.get(),
130                        })
131                    } else {
132                        Ok(next_val)
133                    }
134                }
135                // If we hit this case, then `slice[i]` was zero.
136                None => Err(ChunkOffsetError::NonMonotonic {
137                    start_val,
138                    start: i - 1,
139                    next_val: 0,
140                }),
141            }?;
142        }
143
144        // The last entry in `offset` is one-past the end.
145        Ok(Self { dim: last, offsets })
146    }
147
148    /// Return the number of chunks associated with this mapping.
149    ///
150    /// This will be one-less than the length of the provided slice.
151    pub fn len(&self) -> usize {
152        // This invariant should hold by construction, and allows us to safely subtract
153        // 1 from the length of the underlying `offsets` span.
154        debug_assert!(self.offsets.as_slice().len() >= 2);
155        self.offsets.as_slice().len() - 1
156    }
157
158    /// Return whether the offsets are empty.
159    pub fn is_empty(&self) -> bool {
160        // by class invariant, there must always be at least one chunk.
161        false
162    }
163
164    /// Return the dimensionality of the vector data associated with this chunking schema.
165    pub fn dim(&self) -> usize {
166        self.dim.get()
167    }
168
169    /// Return the dimensionality of the vector data associated with this chunking schema.
170    ///
171    /// By class invariant, the dimensionality must be nonzero, and this expressed in the
172    /// retuen type.
173    ///
174    /// This method cannot fail and will not panic.
175    pub fn dim_nonzero(&self) -> NonZeroUsize {
176        self.dim
177    }
178
179    /// Return a range containing the start and one-past-the-end indices for chunk `i`.
180    ///
181    /// # Panics
182    ///
183    /// Panics if `i >= self.len()`.
184    pub fn at(&self, i: usize) -> core::ops::Range<usize> {
185        assert!(
186            i < self.len(),
187            "index {i} must be less than len {}",
188            self.len()
189        );
190        let slice = self.offsets.as_slice();
191        slice[i]..slice[i + 1]
192    }
193
194    /// Return `self` as a view.
195    pub fn as_view(&self) -> ChunkOffsetsView<'_> {
196        ChunkOffsetsBase {
197            dim: self.dim,
198            offsets: self.offsets.as_slice(),
199        }
200    }
201
202    /// Return a `'static` copy of `self`.
203    pub fn to_owned(&self) -> ChunkOffsets {
204        ChunkOffsetsBase {
205            dim: self.dim,
206            offsets: self.offsets.as_slice().into(),
207        }
208    }
209
210    /// Return the underlying data as a slice.
211    pub fn as_slice(&self) -> &[usize] {
212        self.offsets.as_slice()
213    }
214}
215
216pub type ChunkOffsetsView<'a> = ChunkOffsetsBase<&'a [usize]>;
217pub type ChunkOffsets = ChunkOffsetsBase<Box<[usize]>>;
218
219/// Allow chunk offsets view to be converted directly to slices.
220impl<'a> From<ChunkOffsetsView<'a>> for &'a [usize] {
221    fn from(view: ChunkOffsetsView<'a>) -> Self {
222        view.offsets
223    }
224}
225
226impl ChunkOffsets {
227    /// Build a chunk-offset plan that partitions `dim` into `num_chunks`
228    /// near-equal chunks. The first `dim.get() % num_chunks.get()` chunks are
229    /// one element larger than the rest.
230    ///
231    /// Returns an error if the requested partition is not valid (e.g.
232    /// `num_chunks.get() > dim.get()`).
233    pub fn partition(dim: NonZeroUsize, num_chunks: NonZeroUsize) -> Result<Self, PartitionError> {
234        if num_chunks.get() > dim.get() {
235            return Err(PartitionError {
236                num_chunks: num_chunks.get(),
237                dim: dim.get(),
238            });
239        }
240        let mut offsets = vec![0usize; num_chunks.get() + 1].into_boxed_slice();
241        fill_chunk_offsets(dim, &mut offsets);
242        Ok(Self { dim, offsets })
243    }
244}
245
246impl<'a> ChunkOffsetsView<'a> {
247    /// Fill the caller-owned `scratch` buffer with the partition for `dim`
248    /// into `scratch.len() - 1` chunks and return a validated view borrowing it.
249    ///
250    /// See [`ChunkOffsets::partition`] for the partitioning rule.
251    ///
252    /// Returns an error if `scratch.len() < 2` or if the requested partition is not
253    /// valid (e.g. `scratch.len() - 1 > dim.get()`).
254    pub fn partition_into(
255        dim: NonZeroUsize,
256        scratch: &'a mut [usize],
257    ) -> Result<Self, PartitionIntoError> {
258        if scratch.len() < 2 {
259            return Err(PartitionIntoError::ScratchTooSmall(scratch.len()));
260        }
261        let num_chunks = scratch.len() - 1;
262        if num_chunks > dim.get() {
263            return Err(PartitionError {
264                num_chunks,
265                dim: dim.get(),
266            }
267            .into());
268        }
269
270        fill_chunk_offsets(dim, scratch);
271        Ok(Self {
272            dim,
273            offsets: scratch,
274        })
275    }
276}
277
278/// Internal helper: fill `offsets` with the prefix-sum
279/// partitioning of `dimensions` into `num_chunks` near-equal chunks, where
280/// `num_chunks = offsets.len() - 1`.
281///
282/// The first `dimensions.get() % num_chunks` chunks are one element larger than the
283/// rest, so each chunk has size `dimensions.get() / num_chunks` or
284/// `dimensions.get() / num_chunks + 1` and the total covers `[0, dimensions.get()]`.
285///
286/// # Panics
287///
288/// Panics if `offsets.len() <= 1`.
289fn fill_chunk_offsets(dimensions: NonZeroUsize, offsets: &mut [usize]) {
290    let num_chunks = offsets.len() - 1;
291    let dimensions = dimensions.get();
292    let mut chunk_offset: usize = 0;
293    offsets[0] = chunk_offset;
294    for chunk_index in 0..num_chunks {
295        chunk_offset += dimensions / num_chunks;
296        if chunk_index < (dimensions % num_chunks) {
297            chunk_offset += 1;
298        }
299        offsets[chunk_index + 1] = chunk_offset;
300    }
301}
302
303///////////////
304// ChunkView //
305///////////////
306
307/// A view over a slice that partitions the slice into chunks corresponding to a valid
308/// PQ chunking configuration.
309///
310/// This class maintains the invariant that the provided chunking configuration if valid
311/// and that the data being partitioned has the correct length.
312#[derive(Debug, Clone, Copy)]
313pub struct ChunkViewImpl<'a, T>
314where
315    T: DenseData,
316{
317    data: T,
318    offsets: ChunkOffsetsView<'a>,
319}
320
321#[derive(Error, Debug)]
322#[non_exhaustive]
323#[error(
324    "error in chunk view construction, got a slice of length {got} but \
325         the provided chunking schema expects a length of {should}"
326)]
327pub struct ChunkViewError {
328    got: usize,
329    should: usize,
330}
331
332impl<'a, T> ChunkViewImpl<'a, T>
333where
334    T: DenseData,
335{
336    /// Construct a new `ChunkView`.
337    ///
338    /// Returns an error if `data.len() != offsets.dim()`.
339    pub fn new<U>(data: U, offsets: ChunkOffsetsView<'a>) -> Result<Self, ChunkViewError>
340    where
341        T: From<U>,
342    {
343        let data: T = data.into();
344
345        // Use the `offsets` as the source of truth because it is more likely that a
346        // `ChunkOffsetsView` will be constructed once and reused, so is more likely to be
347        // the desired outcome.
348        let got = data.as_slice().len();
349        let should = offsets.dim();
350        if got != should {
351            Err(ChunkViewError { got, should })
352        } else {
353            Ok(Self { data, offsets })
354        }
355    }
356
357    /// Return the number of partitions in the chunking view.
358    pub fn len(&self) -> usize {
359        self.offsets.len()
360    }
361
362    pub fn is_empty(&self) -> bool {
363        self.offsets.is_empty()
364    }
365}
366
367/// Return the `i`th chunk of the view.
368///
369/// # Panics
370///
371/// Panics if `i >= self.len()`.
372impl<T> Index<usize> for ChunkViewImpl<'_, T>
373where
374    T: DenseData,
375{
376    type Output = [T::Elem];
377
378    fn index(&self, i: usize) -> &Self::Output {
379        &(self.data.as_slice())[self.offsets.at(i)]
380    }
381}
382
383/// Return the `i`th chunk of the view.
384///
385/// # Panics
386///
387/// Panics if `i >= self.len()`.
388impl<T> IndexMut<usize> for ChunkViewImpl<'_, T>
389where
390    T: MutDenseData,
391{
392    fn index_mut(&mut self, i: usize) -> &mut Self::Output {
393        &mut (self.data.as_mut_slice())[self.offsets.at(i)]
394    }
395}
396
397pub type ChunkView<'a, T> = ChunkViewImpl<'a, &'a [T]>;
398pub type MutChunkView<'a, T> = ChunkViewImpl<'a, &'a mut [T]>;
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use diskann_utils::lazy_format;
404
405    /// This function is only callable with copyable types.
406    ///
407    /// This lets us test for types we expect to be `Copy`.
408    fn is_copyable<T: Copy>(_x: T) -> bool {
409        true
410    }
411
412    //////////////////
413    // ChunkOffsets //
414    //////////////////
415
416    #[test]
417    fn chunk_offset_happy_path() {
418        let offsets_raw: Vec<usize> = vec![0, 1, 3, 6, 10, 12, 13, 14];
419        let offsets = ChunkOffsetsView::new(offsets_raw.as_slice()).unwrap();
420
421        assert_eq!(offsets.len(), offsets_raw.len() - 1);
422        assert_eq!(offsets.dim(), *offsets_raw.last().unwrap());
423        assert!(!offsets.is_empty());
424
425        assert_eq!(offsets.at(0), 0..1);
426        assert_eq!(offsets.at(1), 1..3);
427        assert_eq!(offsets.at(2), 3..6);
428        assert_eq!(offsets.at(3), 6..10);
429        assert_eq!(offsets.at(4), 10..12);
430        assert_eq!(offsets.at(5), 12..13);
431        assert_eq!(offsets.at(6), 13..14);
432
433        // Finally, make sure the type is copyable.
434        assert!(is_copyable(offsets));
435        // Make sure `as_slice()` properly round-trips.
436        assert_eq!(offsets.as_slice(), offsets_raw.as_slice());
437
438        // `to_owned`
439        let offsets_owned = offsets.to_owned();
440        assert_eq!(offsets_owned.as_slice(), offsets_raw.as_slice());
441        assert_ne!(
442            offsets_owned.as_slice().as_ptr(),
443            offsets_raw.as_slice().as_ptr()
444        );
445        assert_eq!(offsets_owned.dim, offsets.dim);
446
447        // `as_view`.
448        let offsets_view = offsets_owned.as_view();
449        assert_eq!(offsets_view, offsets);
450        // ensure that pointers are preserved.
451        assert_eq!(
452            offsets_view.as_slice().as_ptr(),
453            offsets_owned.as_slice().as_ptr()
454        );
455    }
456
457    #[test]
458    #[should_panic(expected = "index 5 must be less than len 3")]
459    fn chunk_offset_indexing_panic() {
460        let offsets = ChunkOffsets::new(Box::new([0, 1, 2, 3])).unwrap();
461
462        // panics
463        let _ = offsets.at(5);
464    }
465
466    // Construction errors.
467    #[test]
468    fn chunk_offset_construction_errors() {
469        // Pass an empty slice.
470        let offsets = ChunkOffsets::new(Box::new([]));
471        assert_eq!(
472            offsets.unwrap_err().to_string(),
473            "offsets must have a length of at least 2, found 0"
474        );
475
476        // Pass a slice with length 1.
477        let offsets = ChunkOffsets::new(Box::new([0]));
478        assert_eq!(
479            offsets.unwrap_err().to_string(),
480            "offsets must have a length of at least 2, found 1"
481        );
482
483        // Doesn't start with zero.
484        let offsets = ChunkOffsets::new(Box::new([10, 11, 12, 13]));
485        assert_eq!(
486            offsets.unwrap_err().to_string(),
487            "offsets must begin at 0, not 10"
488        );
489
490        // Non-monotonic cases - zero sized chunk
491        let offsets = ChunkOffsets::new(Box::new([0, 10, 20, 30, 30, 40, 41]));
492        assert_eq!(
493            offsets.unwrap_err().to_string(),
494            "offsets must be strictly increasing, instead entry 30 at position 3 \
495            is followed by 30"
496        );
497
498        // Non-monotonic cases - decreasing size
499        let offsets = ChunkOffsets::new(Box::new([0, 10, 9, 10, 20]));
500        assert_eq!(
501            offsets.unwrap_err().to_string(),
502            "offsets must be strictly increasing, instead entry 10 at position 1 \
503            is followed by 9"
504        );
505
506        // Non-monotonic cases - some dimension after the first is zero.
507        let offsets = ChunkOffsets::new(Box::new([0, 10, 11, 12, 0]));
508        assert_eq!(
509            offsets.unwrap_err().to_string(),
510            "offsets must be strictly increasing, instead entry 12 at position 3 \
511            is followed by 0"
512        );
513
514        // Non-monotonic cases - second entry is zero.
515        let offsets = ChunkOffsets::new(Box::new([0, 0, 11, 12, 20]));
516        assert_eq!(
517            offsets.unwrap_err().to_string(),
518            "offsets must be strictly increasing, instead entry 0 at position 0 \
519            is followed by 0"
520        );
521    }
522
523    //////////////////////
524    // partition builders //
525    //////////////////////
526
527    #[test]
528    fn partition_happy_path() {
529        let nz = |x: usize| NonZeroUsize::new(x).unwrap();
530
531        // Even split: 9 / 3 = 3 each.
532        let offsets = ChunkOffsets::partition(nz(9), nz(3)).unwrap();
533        assert_eq!(offsets.as_slice(), &[0, 3, 6, 9]);
534        assert_eq!(offsets.dim(), 9);
535        assert_eq!(offsets.len(), 3);
536
537        // Uneven split: 8 / 3 = 2 r 2 -> first two chunks get an extra element.
538        let offsets = ChunkOffsets::partition(nz(8), nz(3)).unwrap();
539        assert_eq!(offsets.as_slice(), &[0, 3, 6, 8]);
540
541        // Single chunk degenerate case.
542        let offsets = ChunkOffsets::partition(nz(5), nz(1)).unwrap();
543        assert_eq!(offsets.as_slice(), &[0, 5]);
544
545        // dimensions == num_pq_chunks: each chunk is size 1.
546        let offsets = ChunkOffsets::partition(nz(4), nz(4)).unwrap();
547        assert_eq!(offsets.as_slice(), &[0, 1, 2, 3, 4]);
548
549        // The view-into variant matches the owning constructor; num_chunks is
550        // inferred from `scratch.len() - 1`.
551        let mut scratch = [0usize; 4];
552        let view = ChunkOffsetsView::partition_into(nz(8), &mut scratch).unwrap();
553        assert_eq!(view.as_slice(), &[0, 3, 6, 8]);
554        assert_eq!(view.dim(), 8);
555        assert_eq!(view.len(), 3);
556        assert_eq!(scratch.as_slice(), &[0, 3, 6, 8]);
557    }
558
559    #[test]
560    fn partition_construction_errors() {
561        let nz = |x: usize| NonZeroUsize::new(x).unwrap();
562
563        // num_chunks > dim -> TooManyChunks (caught explicitly before partitioning).
564        let err = ChunkOffsets::partition(nz(3), nz(5)).unwrap_err();
565        assert!(
566            matches!(
567                err,
568                PartitionError {
569                    num_chunks: 5,
570                    dim: 3
571                }
572            ),
573            "expected TooManyChunks, got {err:?}"
574        );
575
576        // Scratch length < 2 -> ScratchTooSmall (cannot infer num_chunks).
577        let mut too_short = [0usize; 1];
578        let err = ChunkOffsetsView::partition_into(nz(8), &mut too_short).unwrap_err();
579        assert!(matches!(err, PartitionIntoError::ScratchTooSmall(1)));
580
581        // num_chunks > dim via the view builder too.
582        let mut scratch = [0usize; 6];
583        let err = ChunkOffsetsView::partition_into(nz(3), &mut scratch).unwrap_err();
584        assert!(matches!(
585            err,
586            PartitionIntoError::PartitionError(PartitionError {
587                num_chunks: 5,
588                dim: 3
589            })
590        ));
591    }
592
593    ///////////////
594    // ChunkView //
595    ///////////////
596
597    fn check_chunk_view<T>(
598        view: &ChunkViewImpl<'_, T>,
599        data: &[i32],
600        offsets: &[usize],
601        context: &dyn std::fmt::Display,
602    ) where
603        T: DenseData<Elem = i32>,
604    {
605        assert_eq!(view.len(), offsets.len() - 1, "{}", context);
606
607        // Ensure that each yielded slice matches that we retrieve manually.
608        for i in 0..view.len() {
609            let context = lazy_format!("start = {}, {}", i, context);
610            let start = offsets[i];
611            let stop = offsets[i + 1];
612
613            let expected = &data[start..stop];
614            let retrieved = &view[i];
615
616            assert_eq!(retrieved, expected, "{}", context);
617        }
618    }
619
620    #[test]
621    fn test_immutable_chunkview() {
622        let data: Vec<i32> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
623        //                        |-----|  |--|  |--------|  |
624        //                          c0      c1       c2      c3
625        let offsets: Vec<usize> = vec![0, 3, 5, 9, 10];
626
627        let chunks = ChunkOffsetsView::new(offsets.as_slice()).unwrap();
628        let chunk_view = ChunkView::new(data.as_slice(), chunks).unwrap();
629
630        assert_eq!(chunk_view.len(), offsets.len() - 1);
631        assert_eq!(chunk_view.len(), chunks.len());
632
633        assert!(is_copyable(chunk_view));
634        let context = lazy_format!("chunkview happy path");
635        check_chunk_view(&chunk_view, &data, &offsets, &context);
636    }
637
638    #[test]
639    fn test_chunkview_construction_error() {
640        let data: Vec<i32> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
641        //                        |-----|  |--|  |--------|  |
642        //                          c0      c1       c2      c3
643        let offsets: Vec<usize> = vec![0, 3, 5, 9]; // One too short.
644
645        let chunks = ChunkOffsetsView::new(offsets.as_slice()).unwrap();
646        let chunk_view = ChunkView::new(data.as_slice(), chunks);
647        assert!(chunk_view.is_err());
648        assert_eq!(
649            chunk_view.unwrap_err().to_string(),
650            "error in chunk view construction, got a slice of length 10 but \
651             the provided chunking schema expects a length of 9"
652        );
653    }
654
655    #[test]
656    fn test_mutable_chunkview() {
657        let mut data: Vec<i32> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
658        //                            |-----|  |--|  |--------|  |
659        //                              c0      c1       c2      c3
660        let offsets: Vec<usize> = vec![0, 3, 5, 9, 10];
661
662        // We need to clone the original data before constructing the mutable chunk view
663        // to avoid both a mutable and immutable borrow in the checking function.
664        let data_clone = data.clone();
665
666        let chunks = ChunkOffsetsView::new(offsets.as_slice()).unwrap();
667        let mut chunk_view = MutChunkView::new(data.as_mut_slice(), chunks).unwrap();
668
669        assert_eq!(chunk_view.len(), offsets.len() - 1);
670        assert_eq!(chunk_view.len(), chunks.len());
671
672        let context = lazy_format!("mutchunkview happy path");
673        check_chunk_view(&chunk_view, &data_clone, &offsets, &context);
674
675        // Make sure that we can assign through the view.
676        for i in 0..chunk_view.len() {
677            let i_i32: i32 = i.try_into().unwrap();
678
679            chunk_view[i].iter_mut().for_each(|d| *d = i_i32);
680        }
681
682        // chunk 0
683        assert_eq!(data[0], 0);
684        assert_eq!(data[1], 0);
685        assert_eq!(data[2], 0);
686
687        // chunk 1
688        assert_eq!(data[3], 1);
689        assert_eq!(data[4], 1);
690
691        // chunk 2
692        assert_eq!(data[5], 2);
693        assert_eq!(data[6], 2);
694        assert_eq!(data[7], 2);
695        assert_eq!(data[8], 2);
696
697        // chunk 3
698        assert_eq!(data[9], 3);
699    }
700}