Skip to main content

mpi/
datatype.rs

1//! Datatypes and buffers.
2//!
3//! Mirrors `mpi::datatype` in rsmpi: the [`Equivalence`] trait maps a Rust
4//! type to an MPI datatype, and the [`Buffer`] / [`BufferMut`] traits describe
5//! contiguous send / receive buffers. Varying-count collectives use
6//! [`Partition`] / [`PartitionMut`].
7//!
8//! Because this is a pure-Rust implementation there is no opaque `MPI_Datatype`
9//! handle to manage; a datatype is fully described by [`DatatypeRef`] (a stable
10//! id plus the element size), which is all the runtime needs to move and
11//! type-check bytes.
12
13use crate::Count;
14
15/// Numeric identifiers for the built-in datatypes. These are stable within a
16/// run and are carried on the wire so a receiver can detect a datatype
17/// mismatch.
18pub mod ids {
19    /// `bool` (`MPI_C_BOOL`).
20    pub const BOOL: u32 = 1;
21    /// `i8` (`MPI_INT8_T`).
22    pub const I8: u32 = 2;
23    /// `u8` (`MPI_UINT8_T`).
24    pub const U8: u32 = 3;
25    /// `i16` (`MPI_INT16_T`).
26    pub const I16: u32 = 4;
27    /// `u16` (`MPI_UINT16_T`).
28    pub const U16: u32 = 5;
29    /// `i32` (`MPI_INT32_T`).
30    pub const I32: u32 = 6;
31    /// `u32` (`MPI_UINT32_T`).
32    pub const U32: u32 = 7;
33    /// `i64` (`MPI_INT64_T`).
34    pub const I64: u32 = 8;
35    /// `u64` (`MPI_UINT64_T`).
36    pub const U64: u32 = 9;
37    /// `i128`.
38    pub const I128: u32 = 10;
39    /// `u128`.
40    pub const U128: u32 = 11;
41    /// `isize`.
42    pub const ISIZE: u32 = 12;
43    /// `usize`.
44    pub const USIZE: u32 = 13;
45    /// `f32` (`MPI_FLOAT`).
46    pub const F32: u32 = 14;
47    /// `f64` (`MPI_DOUBLE`).
48    pub const F64: u32 = 15;
49    /// `char` (Unicode scalar value, 4 bytes).
50    pub const CHAR: u32 = 16;
51    /// Base id from which user-defined / derived datatype ids are allocated.
52    pub const USER_BASE: u32 = 1024;
53}
54
55/// The size in bytes of one element of a built-in datatype id, or `1` (treat as
56/// opaque bytes) for user/derived datatypes whose layout the transport cannot
57/// know. Used to byte-swap typed payloads for big-endian interoperability.
58#[cfg(target_endian = "big")]
59pub(crate) fn wire_elem_size(id: u32) -> usize {
60    match id {
61        ids::BOOL | ids::I8 | ids::U8 => 1,
62        ids::I16 | ids::U16 => 2,
63        ids::I32 | ids::U32 | ids::F32 | ids::CHAR => 4,
64        ids::I64 | ids::U64 | ids::ISIZE | ids::USIZE | ids::F64 => 8,
65        ids::I128 | ids::U128 => 16,
66        _ => 1,
67    }
68}
69
70/// A lightweight description of a datatype: a stable id and the size in bytes
71/// of one element.
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub struct DatatypeRef {
74    /// Stable identifier used for on-the-wire type checking.
75    pub id: u32,
76    /// Size of a single element in bytes.
77    pub size: usize,
78}
79
80/// Types that have an equivalent MPI datatype and a well-defined byte
81/// representation.
82///
83/// # Safety
84///
85/// Implementing this trait asserts that the type is "plain old data": it is
86/// `Copy`, contains no padding that carries meaning, no pointers/references,
87/// and can be safely reinterpreted as a `[u8]` of length `size_of::<Self>()`
88/// and reconstructed from such bytes. All built-in impls satisfy this.
89pub unsafe trait Equivalence: Copy + 'static {
90    /// The datatype describing `Self`.
91    fn equivalent_datatype() -> DatatypeRef;
92}
93
94macro_rules! impl_equivalence {
95    ($($t:ty => $id:expr),+ $(,)?) => {$(
96        // SAFETY: all listed types are primitive POD types.
97        unsafe impl Equivalence for $t {
98            fn equivalent_datatype() -> DatatypeRef {
99                DatatypeRef { id: $id, size: ::core::mem::size_of::<$t>() }
100            }
101        }
102    )+};
103}
104
105impl_equivalence! {
106    bool  => ids::BOOL,
107    i8    => ids::I8,
108    u8    => ids::U8,
109    i16   => ids::I16,
110    u16   => ids::U16,
111    i32   => ids::I32,
112    u32   => ids::U32,
113    i64   => ids::I64,
114    u64   => ids::U64,
115    i128  => ids::I128,
116    u128  => ids::U128,
117    isize => ids::ISIZE,
118    usize => ids::USIZE,
119    f32   => ids::F32,
120    f64   => ids::F64,
121    char  => ids::CHAR,
122}
123
124/// Something with a known element count and datatype. The common supertrait of
125/// [`Buffer`] and [`BufferMut`].
126pub trait Collection {
127    /// Number of elements.
128    fn count(&self) -> Count;
129    /// The datatype of the elements.
130    fn as_datatype(&self) -> DatatypeRef;
131}
132
133/// A read-only, contiguous send buffer.
134///
135/// # Safety
136///
137/// [`Buffer::as_bytes`] must return exactly `count * element_size` bytes that
138/// are a valid representation of the buffer's contents.
139pub unsafe trait Buffer: Collection {
140    /// A byte view of the whole buffer.
141    fn as_bytes(&self) -> &[u8];
142}
143
144/// A writable, contiguous receive buffer.
145///
146/// # Safety
147///
148/// [`BufferMut::as_bytes_mut`] must return exactly `count * element_size`
149/// bytes; writing any bit pattern of that length must leave the buffer in a
150/// valid state (guaranteed for [`Equivalence`] POD element types).
151pub unsafe trait BufferMut: Collection {
152    /// A mutable byte view of the whole buffer.
153    fn as_bytes_mut(&mut self) -> &mut [u8];
154
155    /// Place received `bytes` into this buffer.
156    ///
157    /// The default performs a contiguous copy, which is correct for plain
158    /// buffers. Strided / derived-datatype receive buffers (e.g.
159    /// [`MutView`]) override this to scatter the incoming bytes according to
160    /// their type map.
161    fn scatter_from(&mut self, bytes: &[u8]) {
162        let dst = self.as_bytes_mut();
163        let n = dst.len().min(bytes.len());
164        dst[..n].copy_from_slice(&bytes[..n]);
165    }
166}
167
168#[inline]
169fn as_byte_slice<T: Equivalence>(s: &[T]) -> &[u8] {
170    // SAFETY: `T: Equivalence` is POD, so its bytes are always initialized and
171    // meaningful; the length is exactly `size_of_val`.
172    unsafe { core::slice::from_raw_parts(s.as_ptr() as *const u8, core::mem::size_of_val(s)) }
173}
174
175#[inline]
176fn as_byte_slice_mut<T: Equivalence>(s: &mut [T]) -> &mut [u8] {
177    // SAFETY: as above; POD types accept any bit pattern.
178    unsafe { core::slice::from_raw_parts_mut(s.as_mut_ptr() as *mut u8, core::mem::size_of_val(s)) }
179}
180
181// ---- Scalars: a single value is a buffer of count 1. ----
182
183impl<T: Equivalence> Collection for T {
184    fn count(&self) -> Count {
185        1
186    }
187    fn as_datatype(&self) -> DatatypeRef {
188        T::equivalent_datatype()
189    }
190}
191
192unsafe impl<T: Equivalence> Buffer for T {
193    fn as_bytes(&self) -> &[u8] {
194        as_byte_slice(core::slice::from_ref(self))
195    }
196}
197
198unsafe impl<T: Equivalence> BufferMut for T {
199    fn as_bytes_mut(&mut self) -> &mut [u8] {
200        as_byte_slice_mut(core::slice::from_mut(self))
201    }
202}
203
204// ---- Slices. ----
205
206impl<T: Equivalence> Collection for [T] {
207    fn count(&self) -> Count {
208        self.len() as Count
209    }
210    fn as_datatype(&self) -> DatatypeRef {
211        T::equivalent_datatype()
212    }
213}
214
215unsafe impl<T: Equivalence> Buffer for [T] {
216    fn as_bytes(&self) -> &[u8] {
217        as_byte_slice(self)
218    }
219}
220
221unsafe impl<T: Equivalence> BufferMut for [T] {
222    fn as_bytes_mut(&mut self) -> &mut [u8] {
223        as_byte_slice_mut(self)
224    }
225}
226
227// ---- Fixed-size arrays. ----
228
229impl<T: Equivalence, const N: usize> Collection for [T; N] {
230    fn count(&self) -> Count {
231        N as Count
232    }
233    fn as_datatype(&self) -> DatatypeRef {
234        T::equivalent_datatype()
235    }
236}
237
238unsafe impl<T: Equivalence, const N: usize> Buffer for [T; N] {
239    fn as_bytes(&self) -> &[u8] {
240        as_byte_slice(self.as_slice())
241    }
242}
243
244unsafe impl<T: Equivalence, const N: usize> BufferMut for [T; N] {
245    fn as_bytes_mut(&mut self) -> &mut [u8] {
246        as_byte_slice_mut(self.as_mut_slice())
247    }
248}
249
250/// A buffer whose elements are partitioned into per-rank blocks of possibly
251/// differing sizes, described by counts and displacements. Used by the
252/// varying-count ("v") collectives.
253///
254/// # Safety
255///
256/// The counts and displacements must describe blocks that lie within the
257/// underlying buffer.
258pub unsafe trait Partitioned {
259    /// The datatype of one element.
260    fn as_datatype(&self) -> DatatypeRef;
261    /// Per-block element counts.
262    fn counts(&self) -> &[Count];
263    /// Per-block element displacements (in elements from the start).
264    fn displs(&self) -> &[Count];
265}
266
267/// A read-only partitioned buffer (send side of `*v` collectives).
268///
269/// # Safety
270///
271/// [`PartitionedBuffer::as_bytes`] must be valid for reading
272/// `element_size * (max displ+count)` bytes.
273pub unsafe trait PartitionedBuffer: Partitioned {
274    /// Byte view of the whole underlying buffer.
275    fn as_bytes(&self) -> &[u8];
276}
277
278/// A writable partitioned buffer (receive side of `*v` collectives).
279///
280/// # Safety
281///
282/// As [`PartitionedBuffer`], but for mutable access.
283pub unsafe trait PartitionedBufferMut: Partitioned {
284    /// Mutable byte view of the whole underlying buffer.
285    fn as_bytes_mut(&mut self) -> &mut [u8];
286}
287
288/// A read-only view over a buffer partitioned by `counts` at `displs`.
289pub struct Partition<'a, T: Equivalence> {
290    buf: &'a [T],
291    counts: Vec<Count>,
292    displs: Vec<Count>,
293}
294
295impl<'a, T: Equivalence> Partition<'a, T> {
296    /// Build a partition from a buffer, per-block counts and displacements
297    /// (both measured in elements).
298    pub fn new<C, D>(buf: &'a [T], counts: C, displs: D) -> Partition<'a, T>
299    where
300        C: Into<Vec<Count>>,
301        D: Into<Vec<Count>>,
302    {
303        Partition {
304            buf,
305            counts: counts.into(),
306            displs: displs.into(),
307        }
308    }
309}
310
311unsafe impl<T: Equivalence> Partitioned for Partition<'_, T> {
312    fn as_datatype(&self) -> DatatypeRef {
313        T::equivalent_datatype()
314    }
315    fn counts(&self) -> &[Count] {
316        &self.counts
317    }
318    fn displs(&self) -> &[Count] {
319        &self.displs
320    }
321}
322
323unsafe impl<T: Equivalence> PartitionedBuffer for Partition<'_, T> {
324    fn as_bytes(&self) -> &[u8] {
325        as_byte_slice(self.buf)
326    }
327}
328
329/// A mutable view over a buffer partitioned by `counts` at `displs`.
330pub struct PartitionMut<'a, T: Equivalence> {
331    buf: &'a mut [T],
332    counts: Vec<Count>,
333    displs: Vec<Count>,
334}
335
336impl<'a, T: Equivalence> PartitionMut<'a, T> {
337    /// Build a mutable partition from a buffer, per-block counts and
338    /// displacements (both measured in elements).
339    pub fn new<C, D>(buf: &'a mut [T], counts: C, displs: D) -> PartitionMut<'a, T>
340    where
341        C: Into<Vec<Count>>,
342        D: Into<Vec<Count>>,
343    {
344        PartitionMut {
345            buf,
346            counts: counts.into(),
347            displs: displs.into(),
348        }
349    }
350}
351
352unsafe impl<T: Equivalence> Partitioned for PartitionMut<'_, T> {
353    fn as_datatype(&self) -> DatatypeRef {
354        T::equivalent_datatype()
355    }
356    fn counts(&self) -> &[Count] {
357        &self.counts
358    }
359    fn displs(&self) -> &[Count] {
360        &self.displs
361    }
362}
363
364unsafe impl<T: Equivalence> PartitionedBufferMut for PartitionMut<'_, T> {
365    fn as_bytes_mut(&mut self) -> &mut [u8] {
366        as_byte_slice_mut(self.buf)
367    }
368}
369
370/// A user-defined (derived) datatype describing a non-contiguous layout as a
371/// type map: a set of `(byte_offset, byte_length)` blocks repeated with a
372/// fixed byte `extent`. Mirrors rsmpi's `UserDatatype`.
373///
374/// This implementation supports the common constructors — [`contiguous`],
375/// [`vector`], [`indexed`] and [`structured`] — over a primitive base type.
376/// Send a derived layout with [`View`]; receive one with [`MutView`].
377///
378/// [`contiguous`]: UserDatatype::contiguous
379/// [`vector`]: UserDatatype::vector
380/// [`indexed`]: UserDatatype::indexed
381/// [`structured`]: UserDatatype::structured
382#[derive(Clone, Debug)]
383pub struct UserDatatype {
384    id: u32,
385    base: DatatypeRef,
386    /// `(byte_offset, byte_length)` blocks within one element.
387    blocks: Vec<(usize, usize)>,
388    /// The byte extent (stride) of one element of this datatype.
389    extent: usize,
390    /// The number of base elements described by one element of this datatype.
391    base_count: Count,
392}
393
394impl UserDatatype {
395    fn from_blocks(base: DatatypeRef, blocks: Vec<(usize, usize)>, extent: usize) -> UserDatatype {
396        let bytes: usize = blocks.iter().map(|(_, l)| *l).sum();
397        let base_count = (bytes / base.size.max(1)) as Count;
398        UserDatatype {
399            id: ids::USER_BASE + base.id,
400            base,
401            blocks,
402            extent,
403            base_count,
404        }
405    }
406
407    /// `count` contiguous copies of `base` (`MPI_Type_contiguous`).
408    pub fn contiguous(count: Count, base: DatatypeRef) -> UserDatatype {
409        let len = count as usize * base.size;
410        UserDatatype::from_blocks(base, vec![(0, len)], len)
411    }
412
413    /// `count` blocks of `blocklength` base elements, each `stride` base
414    /// elements apart (`MPI_Type_vector`).
415    pub fn vector(
416        count: Count,
417        blocklength: Count,
418        stride: Count,
419        base: DatatypeRef,
420    ) -> UserDatatype {
421        let bl = blocklength as usize * base.size;
422        let mut blocks = Vec::with_capacity(count as usize);
423        for i in 0..count as usize {
424            blocks.push((i * stride as usize * base.size, bl));
425        }
426        let extent = if count == 0 {
427            0
428        } else {
429            ((count as usize - 1) * stride as usize + blocklength as usize) * base.size
430        };
431        UserDatatype::from_blocks(base, blocks, extent)
432    }
433
434    /// Blocks of the given per-block lengths at the given base-element
435    /// displacements (`MPI_Type_indexed`).
436    pub fn indexed(
437        blocklengths: &[Count],
438        displacements: &[Count],
439        base: DatatypeRef,
440    ) -> UserDatatype {
441        assert_eq!(blocklengths.len(), displacements.len());
442        let mut blocks = Vec::with_capacity(blocklengths.len());
443        let mut extent = 0usize;
444        for (bl, d) in blocklengths.iter().zip(displacements) {
445            let off = *d as usize * base.size;
446            let len = *bl as usize * base.size;
447            blocks.push((off, len));
448            extent = extent.max(off + len);
449        }
450        UserDatatype::from_blocks(base, blocks, extent)
451    }
452
453    /// Blocks of the given per-block base-element lengths at explicit byte
454    /// displacements (`MPI_Type_create_struct`, single base type).
455    pub fn structured(
456        blocklengths: &[Count],
457        byte_displacements: &[isize],
458        base: DatatypeRef,
459    ) -> UserDatatype {
460        assert_eq!(blocklengths.len(), byte_displacements.len());
461        let mut blocks = Vec::with_capacity(blocklengths.len());
462        let mut extent = 0usize;
463        for (bl, d) in blocklengths.iter().zip(byte_displacements) {
464            let off = *d as usize;
465            let len = *bl as usize * base.size;
466            blocks.push((off, len));
467            extent = extent.max(off + len);
468        }
469        UserDatatype::from_blocks(base, blocks, extent)
470    }
471
472    /// The datatype's stable id.
473    pub fn id(&self) -> u32 {
474        self.id
475    }
476
477    /// The byte extent (stride) of one element (`MPI_Type_extent`).
478    pub fn extent(&self) -> usize {
479        self.extent
480    }
481
482    /// The number of base elements in one element of this datatype.
483    pub fn base_count(&self) -> Count {
484        self.base_count
485    }
486
487    /// Gather `count` elements of this datatype from `base` into a contiguous
488    /// packed byte buffer.
489    pub fn pack(&self, base: &[u8], count: Count) -> Vec<u8> {
490        let mut out =
491            Vec::with_capacity(count as usize * self.base_count as usize * self.base.size);
492        for e in 0..count as usize {
493            let elem = e * self.extent;
494            for &(off, len) in &self.blocks {
495                out.extend_from_slice(&base[elem + off..elem + off + len]);
496            }
497        }
498        out
499    }
500
501    /// Scatter `count` packed elements back into `base` according to the type
502    /// map.
503    pub fn unpack(&self, packed: &[u8], base: &mut [u8], count: Count) {
504        let mut pos = 0usize;
505        for e in 0..count as usize {
506            let elem = e * self.extent;
507            for &(off, len) in &self.blocks {
508                let end = (pos + len).min(packed.len());
509                let take = end - pos;
510                base[elem + off..elem + off + take].copy_from_slice(&packed[pos..pos + take]);
511                pos += take;
512            }
513        }
514    }
515}
516
517/// A read-only send buffer that applies a [`UserDatatype`] layout to a base
518/// buffer, packing the selected bytes contiguously. Mirrors rsmpi's `View`.
519pub struct View {
520    packed: Vec<u8>,
521    base: DatatypeRef,
522}
523
524impl View {
525    /// Create a view of `count` elements of `datatype` over `buf`.
526    pub fn with_count<B: Buffer + ?Sized>(buf: &B, datatype: &UserDatatype, count: Count) -> View {
527        View {
528            packed: datatype.pack(buf.as_bytes(), count),
529            base: datatype.base,
530        }
531    }
532}
533
534impl Collection for View {
535    fn count(&self) -> Count {
536        (self.packed.len() / self.base.size.max(1)) as Count
537    }
538    fn as_datatype(&self) -> DatatypeRef {
539        self.base
540    }
541}
542
543// SAFETY: `packed` is a contiguous owned byte buffer.
544unsafe impl Buffer for View {
545    fn as_bytes(&self) -> &[u8] {
546        &self.packed
547    }
548}
549
550/// A writable receive buffer that scatters incoming bytes into a base buffer
551/// according to a [`UserDatatype`] layout. Mirrors rsmpi's `MutView`.
552pub struct MutView<'a, B: BufferMut + ?Sized> {
553    base: &'a mut B,
554    datatype: UserDatatype,
555    count: Count,
556}
557
558impl<'a, B: BufferMut + ?Sized> MutView<'a, B> {
559    /// Create a mutable view of `count` elements of `datatype` over `buf`.
560    pub fn with_count(buf: &'a mut B, datatype: UserDatatype, count: Count) -> MutView<'a, B> {
561        MutView {
562            base: buf,
563            datatype,
564            count,
565        }
566    }
567}
568
569impl<B: BufferMut + ?Sized> Collection for MutView<'_, B> {
570    fn count(&self) -> Count {
571        self.datatype.base_count * self.count
572    }
573    fn as_datatype(&self) -> DatatypeRef {
574        self.datatype.base
575    }
576}
577
578// SAFETY: `scatter_from` places received bytes into the base buffer per the
579// type map; `as_bytes_mut` exposes the underlying base bytes.
580unsafe impl<B: BufferMut + ?Sized> BufferMut for MutView<'_, B> {
581    fn as_bytes_mut(&mut self) -> &mut [u8] {
582        self.base.as_bytes_mut()
583    }
584    fn scatter_from(&mut self, bytes: &[u8]) {
585        self.datatype
586            .unpack(bytes, self.base.as_bytes_mut(), self.count);
587    }
588}
589
590#[cfg(test)]
591mod tests {
592    use super::*;
593
594    #[test]
595    fn datatype_ids_and_sizes() {
596        assert_eq!(i32::equivalent_datatype().id, ids::I32);
597        assert_eq!(i32::equivalent_datatype().size, 4);
598        assert_eq!(f64::equivalent_datatype().size, 8);
599        assert_eq!(u8::equivalent_datatype().size, 1);
600    }
601
602    #[test]
603    fn slice_byte_view_roundtrips() {
604        let data: [i32; 3] = [1, 2, 0x0102_0304];
605        let bytes = data.as_slice().as_bytes();
606        assert_eq!(bytes.len(), 12);
607        assert_eq!(<[i32] as Collection>::count(&data[..]), 3);
608        // Reconstruct and compare.
609        let mut out = [0i32; 3];
610        out.as_mut_slice().as_bytes_mut().copy_from_slice(bytes);
611        assert_eq!(out, data);
612    }
613
614    #[test]
615    fn scalar_is_a_buffer_of_one() {
616        let x = 7u64;
617        assert_eq!(Collection::count(&x), 1);
618        assert_eq!(x.as_bytes().len(), 8);
619    }
620
621    #[test]
622    fn vector_datatype_pack_unpack() {
623        // Pick every other i32 from a 6-element base: elements 0,2,4.
624        let dt = UserDatatype::vector(3, 1, 2, i32::equivalent_datatype());
625        let base = [10i32, 11, 20, 21, 30, 31];
626        let packed = dt.pack(base.as_slice().as_bytes(), 1);
627        let got: Vec<i32> = crate::point_to_point::vec_from_bytes(&packed);
628        assert_eq!(got, vec![10, 20, 30]);
629
630        // Round-trip: scatter back into a fresh base of the same shape.
631        let mut out = [0i32; 6];
632        dt.unpack(&packed, out.as_mut_slice().as_bytes_mut(), 1);
633        assert_eq!(out, [10, 0, 20, 0, 30, 0]);
634    }
635
636    #[test]
637    fn contiguous_and_indexed() {
638        let c = UserDatatype::contiguous(4, f64::equivalent_datatype());
639        assert_eq!(c.base_count(), 4);
640        assert_eq!(c.extent(), 32);
641
642        let idx = UserDatatype::indexed(&[2, 1], &[0, 4], i32::equivalent_datatype());
643        let base = [1i32, 2, 3, 4, 5, 6];
644        let packed = idx.pack(base.as_slice().as_bytes(), 1);
645        let got: Vec<i32> = crate::point_to_point::vec_from_bytes(&packed);
646        assert_eq!(got, vec![1, 2, 5]);
647    }
648
649    /// Property test: for many random `vector` layouts, pack-then-unpack into a
650    /// fresh buffer must reproduce exactly the selected elements.
651    #[test]
652    fn vector_pack_unpack_roundtrips() {
653        let mut state: u64 = 0xdead_beef_0000_0001;
654        let mut rng = || {
655            state ^= state << 13;
656            state ^= state >> 7;
657            state ^= state << 17;
658            state
659        };
660        for _ in 0..300 {
661            let count = (rng() % 4) as i32 + 1;
662            let blocklen = (rng() % 3) as i32 + 1;
663            let stride = blocklen + (rng() % 3) as i32; // stride >= blocklen
664            let dt = UserDatatype::vector(count, blocklen, stride, i32::equivalent_datatype());
665            // Build a base buffer large enough for the datatype's extent.
666            let n_elems = dt.extent() / 4;
667            let base: Vec<i32> = (0..n_elems as i32).collect();
668            let packed = dt.pack(base.as_slice().as_bytes(), 1);
669            let got: Vec<i32> = crate::point_to_point::vec_from_bytes(&packed);
670
671            // Reference: gather blocks manually.
672            let mut expected = Vec::new();
673            for c in 0..count {
674                for k in 0..blocklen {
675                    expected.push(c * stride + k);
676                }
677            }
678            assert_eq!(got, expected, "vector pack mismatch");
679
680            // Unpack into a zeroed buffer and check the selected slots.
681            let mut out = vec![0i32; n_elems];
682            dt.unpack(&packed, out.as_mut_slice().as_bytes_mut(), 1);
683            for c in 0..count {
684                for k in 0..blocklen {
685                    let idx = (c * stride + k) as usize;
686                    assert_eq!(out[idx], idx as i32, "vector unpack mismatch");
687                }
688            }
689        }
690    }
691}