Skip to main content

tract_linalg/frame/
pack.rs

1use std::alloc::Layout;
2use std::fmt::{Debug, Display};
3use std::marker::PhantomData;
4use std::ops::Range;
5use std::sync::Arc;
6use tract_data::internal::*;
7
8use crate::mmm::{
9    EagerPackedInput, MMMInputFormat, MMMInputValue, PackedExoticFact, PackedMatrixStorage,
10};
11
12use crate::WeightType;
13
14#[cfg(feature = "multithread-mm")]
15const PACKING_MIN_CHUNK_BYTES: usize = 32 * 1024;
16
17/// Require enough useful input bytes per target executor chunk, excluding padding.
18#[cfg(feature = "multithread-mm")]
19fn packing_has_enough_work(panels: usize, bytes: usize, threads: usize) -> bool {
20    threads > 1 && panels > 1 && bytes / (4 * threads).min(panels) >= PACKING_MIN_CHUNK_BYTES
21}
22
23/// Gate packing before dispatch; the shared element threshold still applies afterwards.
24fn should_parallel_pack(panels: usize, bytes: usize, elements: usize) -> bool {
25    #[cfg(feature = "multithread-mm")]
26    {
27        use crate::multithread::{Executor, current_tract_executor};
28
29        if panels < 2
30            || bytes < 2 * PACKING_MIN_CHUNK_BYTES
31            || elements < crate::multithread::current_threading_element_threshold()
32        {
33            return false;
34        }
35        let threads = match current_tract_executor() {
36            Executor::MultiThread(pool) => pool.current_num_threads(),
37            Executor::RayonGlobal => rayon::current_num_threads(),
38            Executor::SingleThread => 1,
39        };
40        packing_has_enough_work(panels, bytes, threads)
41    }
42    #[cfg(not(feature = "multithread-mm"))]
43    {
44        let _ = (panels, bytes, elements);
45        false
46    }
47}
48
49#[derive(Clone, Eq, PartialEq, Hash)]
50pub struct PackedFormat {
51    pub dt: DatumType,
52    pub r: usize,
53    pub alignment_bytes: usize,
54    pub end_padding_record: usize,
55}
56
57impl MMMInputFormat for PackedFormat {
58    fn prepare_tensor(&self, t: &Tensor, k_axis: usize, mn_axis: usize) -> TractResult<Tensor> {
59        let packed = PackedFormat::pack_tensor(self, t, k_axis, mn_axis)?;
60        Ok(PackedMatrixStorage::new(packed).into_tensor(t.datum_type()))
61    }
62    fn prepare_one_view(
63        &self,
64        t: &TensorView,
65        k_axis: usize,
66        mn_axis: usize,
67    ) -> TractResult<Box<dyn MMMInputValue>> {
68        PackedFormat::pack_tensor_view(self, t, k_axis, mn_axis)
69    }
70
71    fn prepare_one(
72        &self,
73        t: &Tensor,
74        k_axis: usize,
75        mn_axis: usize,
76    ) -> TractResult<Box<dyn MMMInputValue>> {
77        PackedFormat::pack_tensor(self, t, k_axis, mn_axis)
78    }
79
80    fn precursor(&self) -> WeightType {
81        WeightType::Plain(self.dt)
82    }
83
84    fn r(&self) -> usize {
85        self.r
86    }
87
88    fn k_alignment(&self) -> usize {
89        1
90    }
91
92    #[allow(clippy::collapsible_if)]
93    fn merge_with<'o, 'a: 'o, 'b: 'o>(
94        &'a self,
95        other: &'b dyn MMMInputFormat,
96    ) -> Option<&'o dyn MMMInputFormat> {
97        if let Some(other) = other.downcast_ref::<PackedFormat>() {
98            if self.r == other.r && self.dt == other.dt {
99                if self.alignment_bytes % other.alignment_bytes == 0
100                    && self.end_padding_record >= other.end_padding_record
101                {
102                    return Some(self);
103                }
104                if other.alignment_bytes % self.alignment_bytes == 0
105                    && other.end_padding_record >= self.end_padding_record
106                {
107                    return Some(other);
108                }
109            }
110        }
111        None
112    }
113
114    fn mem_size(&self, k: TDim, mn: TDim) -> TDim {
115        self.len(k, mn) * self.dt.size_of()
116    }
117
118    fn extract_at_mn_f16(
119        &self,
120        data: &EagerPackedInput,
121        mn: usize,
122        slice: &mut [f16],
123    ) -> TractResult<()> {
124        ensure!(data.format().dyn_eq(self));
125        ensure!(self.len(data.k(), data.mn()) * self.dt.size_of() == data.packed.len());
126        unsafe {
127            let ptr = data.packed.as_ptr().add(
128                (self.single_panel_len(data.k()) * (mn / self.r) + mn % self.r) * self.dt.size_of(),
129            );
130            for (i, slot) in slice.iter_mut().enumerate() {
131                let ptr = ptr.add(i * self.dt.size_of() * self.r);
132                *slot = if self.dt == f16::datum_type() {
133                    *(ptr as *const f16)
134                } else if self.dt == f32::datum_type() {
135                    f16::from_f32(*(ptr as *const f32))
136                } else {
137                    bail!("Unexpected DT {:?}", self.dt)
138                }
139            }
140        }
141        Ok(())
142    }
143
144    fn extract_at_mn_f32(
145        &self,
146        data: &EagerPackedInput,
147        mn: usize,
148        slice: &mut [f32],
149    ) -> TractResult<()> {
150        ensure!(data.format().dyn_eq(self));
151        ensure!(self.len(data.k(), data.mn()) * self.dt.size_of() == data.packed.len());
152        unsafe {
153            let ptr = data.packed.as_ptr().add(
154                (self.single_panel_len(data.k()) * (mn / self.r) + mn % self.r) * self.dt.size_of(),
155            );
156            for (i, slot) in slice.iter_mut().enumerate() {
157                let ptr = ptr.add(i * self.dt.size_of() * self.r);
158                *slot = if self.dt == f16::datum_type() {
159                    (*(ptr as *const f16)).to_f32()
160                } else if self.dt == f32::datum_type() {
161                    *(ptr as *const f32)
162                } else {
163                    bail!("Unexpected DT {:?}", self.dt)
164                }
165            }
166        }
167        Ok(())
168    }
169}
170
171impl Display for PackedFormat {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        write!(f, "Packed{:?}[{}]", self.dt, self.r)
174    }
175}
176
177impl Debug for PackedFormat {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        write!(
180            f,
181            "Packed{:?}[{}]@{}+{}",
182            self.dt, self.r, self.alignment_bytes, self.end_padding_record
183        )
184    }
185}
186
187impl PackedFormat {
188    pub const fn new(dt: DatumType, nr: usize, alignment_bytes: usize) -> PackedFormat {
189        PackedFormat { dt, r: nr, alignment_bytes, end_padding_record: 1 }
190    }
191
192    pub const fn with_end_padding_record(self, end_padding_record: usize) -> Self {
193        PackedFormat { end_padding_record, ..self }
194    }
195
196    #[inline]
197    pub fn align(self, alignment: usize) -> Self {
198        Self { alignment_bytes: alignment, ..self }
199    }
200
201    #[inline]
202    pub fn alignment(&self) -> usize {
203        self.alignment_bytes
204    }
205
206    #[inline]
207    pub fn panel_width(&self) -> usize {
208        self.r
209    }
210
211    #[inline]
212    pub fn len<D: DimLike>(&self, k: D, n: D) -> D {
213        n.divceil(self.r) * self.single_panel_len(k)
214    }
215
216    #[inline]
217    pub fn single_panel_len<D: DimLike>(&self, k: D) -> D {
218        ((k + self.end_padding_record) * self.r).divceil(self.alignment()) * self.alignment()
219    }
220
221    #[inline]
222    pub fn single_panel_layout(&self, k: usize, item_size: usize) -> Layout {
223        Layout::from_size_align(self.single_panel_len(k) * item_size, self.alignment()).unwrap()
224    }
225
226    pub fn pack_tensor(
227        &self,
228        t: &Tensor,
229        k_axis: usize,
230        mn_axis: usize,
231    ) -> TractResult<Box<dyn MMMInputValue>> {
232        ensure!(t.datum_type().is_copy());
233        self.pack_tensor_view(&t.view(), k_axis, mn_axis)
234    }
235
236    pub fn pack_tensor_view(
237        &self,
238        t: &TensorView,
239        k_axis: usize,
240        mn_axis: usize,
241    ) -> TractResult<Box<dyn MMMInputValue>> {
242        ensure!(k_axis != mn_axis, "k_axis and mn_axis must differ (both are {k_axis})");
243        ensure!(
244            t.datum_type().unquantized() == self.dt.unquantized(),
245            "Attempting to pack for {self} tensor view {t:?}"
246        );
247        let k = t.shape()[k_axis];
248        let mn = t.shape()[mn_axis];
249        let packed_len = self.len(k, mn);
250        let panel_len = self.single_panel_len(k);
251        let panel_bytes = panel_len * t.datum_type().size_of();
252        let strides = t.strides();
253        unsafe {
254            let mut packed = Blob::new_for_size_and_align(
255                t.datum_type().size_of() * packed_len,
256                self.alignment_bytes,
257            );
258            if cfg!(debug_assertions) {
259                packed.as_bytes_mut().fill(0u8);
260            } else if mn % self.r != 0 {
261                // The kernel computes on the last panel's padding lanes before
262                // their results are discarded; garbage bytes there decode to
263                // denormals and stall the fp pipeline. Zero the partial panel.
264                packed.as_bytes_mut()[(mn / self.r) * panel_bytes..].fill(0u8);
265            }
266            let (k_stride, mn_stride) = (strides[k_axis], strides[mn_axis]);
267            if panel_bytes > 0
268                && (mn_stride == 1 || k_stride == 1)
269                && should_parallel_pack(
270                    mn.divceil(self.r),
271                    k * mn * t.datum_type().size_of(),
272                    packed_len,
273                )
274            {
275                // For these two layouts `pack_t` writes a column range from the range's first
276                // panel, so panels split freely; the strided fallback lays out all of mn at once.
277                let src = t.as_ptr_unchecked::<u8>() as usize;
278                let dt = t.datum_type();
279                crate::multithread::par_chunks_mut(
280                    packed.as_bytes_mut(),
281                    panel_bytes,
282                    packed_len,
283                    |first, chunk| {
284                        let panels = chunk.len() / panel_bytes;
285                        let mn_range = first * self.r..((first + panels) * self.r);
286                        dispatch_copy!(Self::pack_t(dt)(
287                            self,
288                            chunk.as_mut_ptr() as _,
289                            src as *const _,
290                            mn,
291                            k_stride,
292                            mn_stride,
293                            0..k,
294                            mn_range
295                        ));
296                        Ok(())
297                    },
298                )?;
299            } else {
300                dispatch_copy!(Self::pack_t(t.datum_type())(
301                    self,
302                    packed.as_mut_ptr() as _,
303                    t.as_ptr_unchecked(),
304                    mn,
305                    k_stride,
306                    mn_stride,
307                    0..k,
308                    0..mn
309                ));
310            }
311            Ok(Box::new(EagerPackedInput {
312                fact: PackedExoticFact { format: Box::new(self.clone()), mn: mn.to_dim(), k },
313                packed: packed.into(),
314                panel_bytes,
315                mn,
316            }))
317        }
318    }
319
320    /// Allocate a packed buffer for a `k` x `mn` operand, to be refilled by
321    /// [`Self::repack_tensor_view`] on each use instead of reallocated.
322    ///
323    /// An operand that changes every call but keeps its shape -- a recurrent
324    /// state stepped through a sequence, say -- would otherwise pay a fresh
325    /// allocation per step for a buffer whose size never changes.
326    pub fn new_packed_buffer(&self, k: usize, mn: usize) -> TractResult<EagerPackedInput> {
327        ensure!(self.dt.is_copy());
328        let packed_len = self.len(k, mn);
329        let packed = unsafe {
330            let mut blob =
331                Blob::new_for_size_and_align(self.dt.size_of() * packed_len, self.alignment_bytes);
332            blob.as_bytes_mut().fill(0u8);
333            blob
334        };
335        Ok(EagerPackedInput {
336            fact: PackedExoticFact { format: Box::new(self.clone()), mn: mn.to_dim(), k },
337            packed: packed.into(),
338            panel_bytes: self.single_panel_len(k) * self.dt.size_of(),
339            mn,
340        })
341    }
342
343    /// Refill a buffer from [`Self::new_packed_buffer`] with `t`, in place.
344    ///
345    /// `dst` must have been built by this format for the same `k` and `mn` as
346    /// `t` presents on `k_axis` and `mn_axis`, and must not be shared -- a
347    /// second holder of its panels would see them change underneath.
348    pub fn repack_tensor_view(
349        &self,
350        dst: &mut EagerPackedInput,
351        t: &TensorView,
352        k_axis: usize,
353        mn_axis: usize,
354    ) -> TractResult<()> {
355        ensure!(
356            t.datum_type().unquantized() == self.dt.unquantized(),
357            "Attempting to pack for {self} tensor view {t:?}"
358        );
359        let k = t.shape()[k_axis];
360        let mn = t.shape()[mn_axis];
361        ensure!(
362            dst.fact.k == k && dst.mn == mn && dst.fact.format.dyn_eq(self),
363            "Packed buffer is {:?} for k={} mn={}, repacking a k={k} mn={mn} view",
364            dst.fact.format,
365            dst.fact.k,
366            dst.mn
367        );
368        let strides = t.strides();
369        let packed = Arc::get_mut(&mut dst.packed).context("Packed buffer is shared")?;
370        unsafe {
371            dispatch_copy!(Self::pack_t(t.datum_type())(
372                self,
373                packed.as_mut_ptr() as _,
374                t.as_ptr_unchecked(),
375                mn,
376                strides[k_axis],
377                strides[mn_axis],
378                0..k,
379                0..mn
380            ));
381        }
382        Ok(())
383    }
384
385    pub unsafe fn pack<'a, 'b>(
386        &self,
387        pb: impl std::borrow::BorrowMut<TensorView<'a>>,
388        b: impl std::borrow::Borrow<TensorView<'b>>,
389        k_axis: usize,
390        mn_axis: usize,
391    ) {
392        let k = b.borrow().shape()[k_axis];
393        let mn = b.borrow().shape()[mn_axis];
394        unsafe { self.pack_segment(pb, b, k_axis, mn_axis, 0..k, 0..mn) };
395    }
396
397
398    #[allow(clippy::too_many_arguments)]
399    #[rustfmt::skip]
400    pub unsafe fn pack_t<T: Datum + Copy>(
401        &self,
402        pb: *mut T,
403        b: *const T,
404        mn: usize,
405        k_stride: isize,
406        mn_stride: isize,
407        k_range: Range<usize>,
408        mn_range: Range<usize>,
409        ) { unsafe {
410        if k_range.len() == 0 || mn_range.len() == 0 {
411            return
412        }
413        if self.r == 1 && k_stride == 1 && mn == 1 {
414            pb.copy_from_nonoverlapping(b.add(k_range.start), k_range.len())
415        } else if mn_stride == 1 {
416            let size_of = T::datum_type().size_of();
417            let rbytes = self.r * size_of;
418            let mn_valid_end = mn_range.end.min(mn);
419            let mn_range_bytes = mn_range.start * size_of..mn_valid_end * size_of;
420            let k_stride_bytes = k_stride * size_of as isize;
421            let bb = b as *const u8;
422            let pbb = pb as *mut u8;
423            let panel_len = self.single_panel_len(k_range.len()) * size_of;
424            match rbytes {
425                16 => pack_mn_major::<[u8; 16]>(bb, pbb, panel_len, k_stride_bytes, mn_range_bytes, k_range),
426                24 => pack_mn_major::<[u8; 24]>(bb, pbb, panel_len, k_stride_bytes, mn_range_bytes, k_range),
427                32 => pack_mn_major::<[u8; 32]>(bb, pbb, panel_len, k_stride_bytes, mn_range_bytes, k_range),
428                48 => pack_mn_major::<[u8; 48]>(bb, pbb, panel_len, k_stride_bytes, mn_range_bytes, k_range),
429                64 => pack_mn_major::<[u8; 64]>(bb, pbb, panel_len, k_stride_bytes, mn_range_bytes, k_range),
430                96 => pack_mn_major::<[u8; 96]>(bb, pbb, panel_len, k_stride_bytes, mn_range_bytes, k_range),
431                128 => pack_mn_major::<[u8; 128]>(bb, pbb, panel_len, k_stride_bytes, mn_range_bytes, k_range),
432                _ => {
433                    let mut packer = self.write_with_k_outer(pb, k_range.len(), mn_range.len());
434                    for k in k_range {
435                        for x in mn_range.start..mn_valid_end {
436                            packer.write(*b.offset(x as isize + k_stride * k as isize))
437                        }
438                        for _x in mn_valid_end..mn_range.end {
439                            packer.write(T::default())
440                        }
441                    }
442                }
443            }
444        } else if k_stride == 1 {
445            // just ignore invalid mn_range
446            let mn_valid_end = mn_range.end.min(mn);
447            if mn_valid_end > mn_range.start {
448                pack_k_major(
449                    b.offset(mn_range.start as isize * mn_stride + k_range.start as isize),
450                    pb,
451                    self.single_panel_len(k_range.len()),
452                    self.r,
453                    mn_stride,
454                    k_range.len(),
455                    mn_valid_end - mn_range.start,
456                )
457            }
458        } else {
459            let mut packer = self.write_with_k_outer(pb, k_range.len(), mn);
460            let mn_valid_end = mn_range.end.min(mn);
461            for k in k_range {
462                for x in mn_range.start..mn_valid_end {
463                    packer.write(*b.offset(x as isize * mn_stride + k_stride * k as isize))
464                }
465                for _x in mn_valid_end..mn_range.end {
466                    packer.write(T::default())
467                }
468            }
469        }
470    }}
471
472    #[inline]
473    pub unsafe fn pack_segment<'a, 'b>(
474        &self,
475        mut pb: impl std::borrow::BorrowMut<TensorView<'a>>,
476        b: impl std::borrow::Borrow<TensorView<'b>>,
477        k_axis: usize,
478        mn_axis: usize,
479        k_range: Range<usize>,
480        mn_range: Range<usize>,
481    ) {
482        debug_assert!(pb.borrow().len() >= self.len(k_range.len(), mn_range.len()));
483        let pb = pb.borrow_mut();
484        let b = b.borrow();
485        let dt = pb.datum_type();
486        unsafe {
487            dispatch_copy!(Self::pack_t(dt)(
488                self,
489                pb.as_ptr_mut_unchecked(),
490                b.as_ptr_unchecked(),
491                b.shape()[mn_axis],
492                b.strides()[k_axis],
493                b.strides()[mn_axis],
494                k_range,
495                mn_range
496            ));
497        }
498    }
499
500    pub fn write_with_k_outer<'p, T: Copy + Debug>(
501        &self,
502        pb: *mut T,
503        k: usize,
504        mn: usize,
505    ) -> KOutWriter<'p, T> {
506        KOutWriter::new(pb, self.r, self.single_panel_len(k), mn, k)
507    }
508
509    pub fn write_single_panel_with_k_outer<'p, T: Copy + Debug>(
510        &self,
511        pb: *mut T,
512    ) -> KOutSinglePanelWriter<'p, T> {
513        KOutSinglePanelWriter::new(pb)
514    }
515
516    pub fn write_with_k_inner<'p, T: Copy + Debug>(
517        &self,
518        pb: *mut T,
519        k: usize,
520        mn: usize,
521    ) -> KInWriter<'p, T> {
522        let panel_len = self.single_panel_len(k);
523        KInWriter::new(pb, panel_len, self.r, mn, k)
524    }
525}
526
527pub trait PackingWriter<T: Copy> {
528    fn write(&mut self, t: T);
529
530    /// Write a contiguous slice of values. The default implementation falls
531    /// back to per-element `write`; concrete writers may override with a
532    /// `memcpy`-class fast path when the destination layout permits it.
533    ///
534    /// The output produced by `write_slice(s)` must be byte-identical to
535    /// `for &t in s { self.write(t); }` for any input.
536    #[inline]
537    fn write_slice(&mut self, ts: &[T]) {
538        for t in ts {
539            self.write(*t);
540        }
541    }
542}
543
544#[derive(Debug)]
545pub struct KOutSinglePanelWriter<'p, T>
546where
547    T: Copy + std::fmt::Debug,
548{
549    ptr: *mut T,
550    _phantom: PhantomData<&'p T>,
551}
552
553impl<'p, T> KOutSinglePanelWriter<'p, T>
554where
555    T: Copy + std::fmt::Debug,
556{
557    pub fn new(ptr: *mut T) -> KOutSinglePanelWriter<'p, T> {
558        KOutSinglePanelWriter { ptr, _phantom: PhantomData }
559    }
560}
561
562impl<T> PackingWriter<T> for KOutSinglePanelWriter<'_, T>
563where
564    T: Copy + std::fmt::Debug,
565{
566    #[inline(always)]
567    fn write(&mut self, t: T) {
568        unsafe {
569            *self.ptr = t;
570            self.ptr = self.ptr.offset(1);
571        }
572    }
573
574    #[inline]
575    fn write_slice(&mut self, ts: &[T]) {
576        // KOutSinglePanelWriter writes elements consecutively with no panel
577        // boundaries. A direct `copy_nonoverlapping` is byte-identical to the
578        // per-element loop.
579        unsafe {
580            std::ptr::copy_nonoverlapping(ts.as_ptr(), self.ptr, ts.len());
581            self.ptr = self.ptr.add(ts.len());
582        }
583    }
584}
585
586#[derive(Debug)]
587pub struct KOutWriter<'p, T>
588where
589    T: Copy + std::fmt::Debug,
590{
591    ptr: *mut T,
592    panels: usize,
593    panel_width: usize,
594    last_panel_width: usize,
595    remain: usize,
596    current_panel: usize,
597    next_panel: isize,
598    next_lane: isize,
599    _phantom: PhantomData<&'p T>,
600}
601
602impl<'p, T> KOutWriter<'p, T>
603where
604    T: Copy + std::fmt::Debug,
605{
606    pub fn new(
607        ptr: *mut T,
608        panel_width: usize,
609        panel_len: usize,
610        mn: usize,
611        _k: usize,
612    ) -> KOutWriter<'p, T> {
613        let panels = mn.divceil(panel_width);
614        let last_panel_width = mn - (panels - 1) * panel_width;
615        KOutWriter {
616            ptr,
617            panels,
618            panel_width,
619            last_panel_width,
620            remain: if panels > 1 { panel_width } else { last_panel_width },
621            current_panel: 0,
622            next_panel: (panel_len - panel_width) as isize,
623            next_lane: (panel_width - last_panel_width) as isize
624                - (panel_len * (panels - 1)) as isize,
625            _phantom: PhantomData,
626        }
627    }
628}
629
630impl<T> PackingWriter<T> for KOutWriter<'_, T>
631where
632    T: Copy + std::fmt::Debug,
633{
634    #[inline(always)]
635    fn write(&mut self, t: T) {
636        unsafe {
637            *self.ptr = t;
638            self.remain -= 1;
639            self.ptr = self.ptr.offset(1);
640            if self.remain == 0 {
641                self.current_panel += 1;
642                if self.current_panel == self.panels {
643                    self.ptr = self.ptr.offset(self.next_lane);
644                    self.current_panel = 0;
645                } else {
646                    self.ptr = self.ptr.offset(self.next_panel);
647                }
648                if self.current_panel == self.panels - 1 {
649                    self.remain = self.last_panel_width;
650                } else {
651                    self.remain = self.panel_width;
652                }
653            }
654        }
655    }
656
657    #[inline]
658    fn write_slice(&mut self, ts: &[T]) {
659        // Fast path: the slice fits entirely within the current panel. Writes
660        // are then guaranteed to be `ts.len()` consecutive memory locations
661        // followed by the same panel/lane bookkeeping the per-element path
662        // performs. This produces byte-identical output to a per-element loop.
663        //
664        // When the slice would cross a panel boundary, fall back to the
665        // per-element path so all transition logic stays in one place.
666        let n = ts.len();
667        if n == 0 {
668            return;
669        }
670        if n < self.remain {
671            // Strictly inside the current panel: bulk copy, then advance.
672            unsafe {
673                std::ptr::copy_nonoverlapping(ts.as_ptr(), self.ptr, n);
674                self.ptr = self.ptr.add(n);
675            }
676            self.remain -= n;
677        } else if n == self.remain {
678            // Exactly fills the current panel: bulk copy, then run the same
679            // panel-transition bookkeeping that `write` does on its final
680            // element. The transition is performed unconditionally here
681            // (rather than calling `write` for the last element) to keep the
682            // semantics identical even when the trait is inlined separately.
683            unsafe {
684                std::ptr::copy_nonoverlapping(ts.as_ptr(), self.ptr, n);
685                self.ptr = self.ptr.add(n);
686                self.current_panel += 1;
687                if self.current_panel == self.panels {
688                    self.ptr = self.ptr.offset(self.next_lane);
689                    self.current_panel = 0;
690                } else {
691                    self.ptr = self.ptr.offset(self.next_panel);
692                }
693                if self.current_panel == self.panels - 1 {
694                    self.remain = self.last_panel_width;
695                } else {
696                    self.remain = self.panel_width;
697                }
698            }
699        } else {
700            // Spans a panel boundary. Fall back to per-element writes so the
701            // panel-transition state machine handles every step.
702            for t in ts {
703                self.write(*t);
704            }
705        }
706    }
707}
708
709#[derive(Debug)]
710pub struct KInWriter<'p, T>
711where
712    T: Copy + Debug,
713{
714    ptr: *mut T,
715    k: usize,
716    panels: usize,
717    panel_width: usize,
718    last_panel_width: usize,
719    remain_on_k: usize,
720    remain_on_mn: usize,
721    current_panel: usize,
722    next_mn_offset: isize,
723    next_panel_offset: isize,
724    _phantom: PhantomData<&'p T>,
725}
726
727impl<'p, T> KInWriter<'p, T>
728where
729    T: Copy + Debug,
730{
731    pub fn new(
732        ptr: *mut T,
733        panel_len: usize,
734        panel_width: usize,
735        mn: usize,
736        k: usize,
737    ) -> KInWriter<'p, T> {
738        assert!(panel_width > 0, "panel_width must be non-zero");
739        assert!(
740            panel_len > 0 || (k == 0 && mn == 0),
741            "panel_len must be non-zero when k or mn is non-zero"
742        );
743        assert!(k.checked_mul(panel_width).is_some(), "k * panel_width overflows");
744        let panels = mn.divceil(panel_width);
745        let last_panel_width = mn - (panels - 1) * panel_width;
746        KInWriter {
747            ptr,
748            k,
749            panels,
750            panel_width,
751            last_panel_width,
752            remain_on_k: k,
753            remain_on_mn: if panels == 1 { last_panel_width } else { panel_width },
754            current_panel: 0,
755            next_mn_offset: 1 - (k * panel_width) as isize,
756            next_panel_offset: panel_len as isize - (k * panel_width + panel_width - 1) as isize,
757            //                 ^ next panel     ^    ^ rewind left ^   ^ rewind up   ^
758            _phantom: PhantomData,
759        }
760    }
761}
762
763impl<T> PackingWriter<T> for KInWriter<'_, T>
764where
765    T: Copy + std::fmt::Debug,
766{
767    #[inline(always)]
768    fn write(&mut self, t: T) {
769        unsafe {
770            *self.ptr = t;
771            self.remain_on_k -= 1;
772            self.ptr = self.ptr.add(self.panel_width);
773            if self.remain_on_k == 0 {
774                self.remain_on_k = self.k;
775                self.remain_on_mn -= 1;
776                if self.remain_on_mn > 0 {
777                    self.ptr = self.ptr.offset(self.next_mn_offset);
778                } else {
779                    self.ptr = self.ptr.offset(self.next_panel_offset);
780                    self.current_panel += 1;
781                    if self.current_panel == self.panels - 1 {
782                        self.remain_on_mn = self.last_panel_width;
783                    } else {
784                        self.remain_on_mn = self.panel_width;
785                    }
786                }
787            }
788        }
789    }
790}
791
792#[inline(never)]
793unsafe fn pack_mn_major<Chunk: Copy>(
794    b: *const u8,
795    packed: *mut u8,
796    panel_len: usize,
797    k_stride_bytes: isize,
798    mn_range_bytes: Range<usize>,
799    k_range: Range<usize>,
800) {
801    unsafe {
802        let mnr = std::mem::size_of::<Chunk>();
803        let full_panes = mn_range_bytes.len() / mnr;
804        let partial_pane = mn_range_bytes.len() % mnr;
805        for k in 0..k_range.len() {
806            let mut p_row = packed.add(k * mnr);
807            let mut b_row = b.offset(
808                (k_range.start + k) as isize * k_stride_bytes + mn_range_bytes.start as isize,
809            );
810            for _ in 0..full_panes {
811                p_row.copy_from_nonoverlapping(b_row, mnr);
812                p_row = p_row.add(panel_len);
813                b_row = b_row.add(mnr);
814            }
815            if partial_pane > 0 {
816                p_row.copy_from_nonoverlapping(b_row, partial_pane);
817            }
818        }
819    }
820}
821
822/// Smallest k-contiguous block (in elements) worth transposing with the armv7
823/// NEON tile rather than the scalar tail. Below it the tile's setup does not
824/// amortise on armv7's narrow in-order NEON; the crossover sits in the gap
825/// between the small activation packs (≤3072) and the large ones (≥5120) that
826/// the wake-word models produce, and holds on both cortex-a7 and cortex-a9.
827const ARMV7_TILE_MIN_ELEMS: usize = 4096;
828
829/// Whether the 32-bit arm NEON transpose leaves may run: their mnemonics are
830/// only valid, and `pack_k_major` only routes to them, when the CPU has NEON.
831#[cfg(target_arch = "arm")]
832#[inline]
833fn armv7_has_neon() -> bool {
834    crate::arm32::has_neon()
835}
836
837#[cfg(not(target_arch = "arm"))]
838#[inline]
839fn armv7_has_neon() -> bool {
840    false
841}
842
843/// Pack a k-contiguous source block: transpose it into the k-inner packed
844/// layout, where source element `(mn, k)` of the block lands at
845/// `(mn / r) * panel_len + k * r + mn % r`. `b` points at element `(0, 0)`, and
846/// `mn_len` counts valid mn columns only: nothing outside the block is read.
847///
848/// The result must stay byte-identical to feeding [`KInWriter`] mn-outer /
849/// k-inner. Stores are strided by `r`, so the block moves as 4x4 tiles, k-outer
850/// so that each panel is filled front to back; the tails go element by element.
851#[inline(never)]
852unsafe fn pack_k_major<T: Copy>(
853    b: *const T,
854    packed: *mut T,
855    panel_len: usize,
856    r: usize,
857    mn_stride: isize,
858    k_len: usize,
859    mn_len: usize,
860) {
861    unsafe {
862        // The tile is vectorised on aarch64 (always) and on 32-bit arm only for
863        // the 2- and 4-byte NEON leaves, and there only when NEON is present.
864        // Any other arm case would spill 16 live tile values through the stack,
865        // so it takes the byte-identical scalar tail instead. armv7's weak NEON
866        // also loses to the scalar store on small blocks, where the tile setup
867        // does not amortise; below ARMV7_TILE_MIN_ELEMS it takes the tail too.
868        let tile = if cfg!(target_arch = "arm") {
869            armv7_has_neon()
870                && matches!(std::mem::size_of::<T>(), 2 | 4)
871                && k_len * mn_len >= ARMV7_TILE_MIN_ELEMS
872        } else {
873            true
874        };
875        for panel in 0..mn_len.divceil(r) {
876            let panel_mn = panel * r;
877            let panel_width = r.min(mn_len - panel_mn);
878            let src = b.offset(panel_mn as isize * mn_stride);
879            let dst = packed.add(panel * panel_len);
880            let tiled_mn = if tile { panel_width / 4 * 4 } else { 0 };
881            let tiled_k = k_len / 4 * 4;
882            for k in (0..tiled_k).step_by(4) {
883                for x in (0..tiled_mn).step_by(4) {
884                    transpose_4x4(
885                        src.offset(x as isize * mn_stride + k as isize),
886                        mn_stride,
887                        dst.add(k * r + x),
888                        r,
889                    );
890                }
891            }
892            for k in tiled_k..k_len {
893                for x in 0..tiled_mn {
894                    *dst.add(k * r + x) = *src.offset(x as isize * mn_stride + k as isize);
895                }
896            }
897            for x in tiled_mn..panel_width {
898                let row = src.offset(x as isize * mn_stride);
899                for k in 0..k_len {
900                    *dst.add(k * r + x) = *row.add(k);
901                }
902            }
903        }
904    }
905}
906
907/// Transpose a 4x4 tile: `src` rows are `src_stride` apart with contiguous
908/// elements, `dst` rows are `dst_stride` apart with contiguous elements. Both
909/// strides count elements and may leave the tiles unaligned. Specialised by
910/// element width where a vector transpose exists, portable everywhere else.
911#[inline(always)]
912unsafe fn transpose_4x4<T: Copy>(src: *const T, src_stride: isize, dst: *mut T, dst_stride: usize) {
913    unsafe {
914        // Alignment is part of the test: a 4-byte T of alignment 2 (Complex<i16>)
915        // must not be moved through a lane type it cannot be aligned for.
916        #[cfg(target_arch = "aarch64")]
917        if std::mem::size_of::<T>() == 4 && std::mem::align_of::<T>() == 4 {
918            transpose_4x4_neon_32(src as _, src_stride, dst as _, dst_stride);
919            return;
920        }
921        #[cfg(target_arch = "aarch64")]
922        if std::mem::size_of::<T>() == 2 && std::mem::align_of::<T>() == 2 {
923            transpose_4x4_neon_16(src as _, src_stride, dst as _, dst_stride);
924            return;
925        }
926        // 32-bit arm: NEON via asm, since both the intrinsics and
927        // `#[target_feature(enable = "neon")]` are unstable on this target.
928        // Reached only through pack_k_major's tiled path, which on arm runs
929        // solely when has_neon() is true, so the NEON these emit is present.
930        #[cfg(target_arch = "arm")]
931        if std::mem::size_of::<T>() == 4 && std::mem::align_of::<T>() == 4 {
932            transpose_4x4_neon_armv7_32(src as _, src_stride, dst as _, dst_stride);
933            return;
934        }
935        #[cfg(target_arch = "arm")]
936        if std::mem::size_of::<T>() == 2 && std::mem::align_of::<T>() == 2 {
937            transpose_4x4_neon_armv7_16(src as _, src_stride, dst as _, dst_stride);
938            return;
939        }
940        let tile: [[T; 4]; 4] = std::array::from_fn(|i| {
941            let row = src.offset(i as isize * src_stride);
942            std::array::from_fn(|j| *row.add(j))
943        });
944        for j in 0..4 {
945            let out = dst.add(j * dst_stride);
946            for (i, row) in tile.iter().enumerate() {
947                *out.add(i) = row[j];
948            }
949        }
950    }
951}
952
953/// 4x4 transpose of 32-bit lanes: four `ld1`, eight `trn`, four `st1`.
954#[cfg(target_arch = "aarch64")]
955#[target_feature(enable = "neon")]
956unsafe fn transpose_4x4_neon_32(
957    src: *const u32,
958    src_stride: isize,
959    dst: *mut u32,
960    dst_stride: usize,
961) {
962    use std::arch::aarch64::*;
963    unsafe {
964        let a = vld1q_u32(src);
965        let b = vld1q_u32(src.offset(src_stride));
966        let c = vld1q_u32(src.offset(2 * src_stride));
967        let d = vld1q_u32(src.offset(3 * src_stride));
968        let ab_even = vreinterpretq_u64_u32(vtrn1q_u32(a, b));
969        let ab_odd = vreinterpretq_u64_u32(vtrn2q_u32(a, b));
970        let cd_even = vreinterpretq_u64_u32(vtrn1q_u32(c, d));
971        let cd_odd = vreinterpretq_u64_u32(vtrn2q_u32(c, d));
972        vst1q_u32(dst, vreinterpretq_u32_u64(vtrn1q_u64(ab_even, cd_even)));
973        vst1q_u32(dst.add(dst_stride), vreinterpretq_u32_u64(vtrn1q_u64(ab_odd, cd_odd)));
974        vst1q_u32(dst.add(2 * dst_stride), vreinterpretq_u32_u64(vtrn2q_u64(ab_even, cd_even)));
975        vst1q_u32(dst.add(3 * dst_stride), vreinterpretq_u32_u64(vtrn2q_u64(ab_odd, cd_odd)));
976    }
977}
978
979/// 4x4 transpose of 16-bit lanes, on 64-bit halves of the vector registers.
980#[cfg(target_arch = "aarch64")]
981#[target_feature(enable = "neon")]
982unsafe fn transpose_4x4_neon_16(
983    src: *const u16,
984    src_stride: isize,
985    dst: *mut u16,
986    dst_stride: usize,
987) {
988    use std::arch::aarch64::*;
989    unsafe {
990        let a = vld1_u16(src);
991        let b = vld1_u16(src.offset(src_stride));
992        let c = vld1_u16(src.offset(2 * src_stride));
993        let d = vld1_u16(src.offset(3 * src_stride));
994        let ab_even = vreinterpret_u32_u16(vtrn1_u16(a, b));
995        let ab_odd = vreinterpret_u32_u16(vtrn2_u16(a, b));
996        let cd_even = vreinterpret_u32_u16(vtrn1_u16(c, d));
997        let cd_odd = vreinterpret_u32_u16(vtrn2_u16(c, d));
998        vst1_u16(dst, vreinterpret_u16_u32(vtrn1_u32(ab_even, cd_even)));
999        vst1_u16(dst.add(dst_stride), vreinterpret_u16_u32(vtrn1_u32(ab_odd, cd_odd)));
1000        vst1_u16(dst.add(2 * dst_stride), vreinterpret_u16_u32(vtrn2_u32(ab_even, cd_even)));
1001        vst1_u16(dst.add(3 * dst_stride), vreinterpret_u16_u32(vtrn2_u32(ab_odd, cd_odd)));
1002    }
1003}
1004
1005/// 4x4 transpose of 32-bit lanes on 32-bit arm: four `vld1.32`, two `vtrn.32`,
1006/// two `vswp`, four `vst1.32`, all in q0-q3. Strides count elements. NEON is
1007/// enabled locally with `.fpu neon` because it cannot be turned on through
1008/// `-C target-feature` on this target's stable channel.
1009///
1010/// # Safety
1011/// The CPU must have NEON: there is no `#[target_feature(enable = "neon")]` on
1012/// this target to assert it (unstable), so the caller guarantees it, which
1013/// `pack_k_major` does by only tiling under `arm32::has_neon()`.
1014#[cfg(target_arch = "arm")]
1015#[inline(always)]
1016unsafe fn transpose_4x4_neon_armv7_32(
1017    src: *const u32,
1018    src_stride: isize,
1019    dst: *mut u32,
1020    dst_stride: usize,
1021) {
1022    use std::arch::asm;
1023    let ss = src_stride * 4;
1024    let ds = (dst_stride * 4) as isize;
1025    let src = src as *const u8;
1026    let dst = dst as *mut u8;
1027    unsafe {
1028        asm!(
1029            ".fpu neon",
1030            "vld1.32 {{d0, d1}}, [{s0}]",
1031            "vld1.32 {{d2, d3}}, [{s1}]",
1032            "vld1.32 {{d4, d5}}, [{s2}]",
1033            "vld1.32 {{d6, d7}}, [{s3}]",
1034            "vtrn.32 q0, q1",
1035            "vtrn.32 q2, q3",
1036            "vswp d1, d4",
1037            "vswp d3, d6",
1038            "vst1.32 {{d0, d1}}, [{o0}]",
1039            "vst1.32 {{d2, d3}}, [{o1}]",
1040            "vst1.32 {{d4, d5}}, [{o2}]",
1041            "vst1.32 {{d6, d7}}, [{o3}]",
1042            s0 = in(reg) src,
1043            s1 = in(reg) src.offset(ss),
1044            s2 = in(reg) src.offset(2 * ss),
1045            s3 = in(reg) src.offset(3 * ss),
1046            o0 = in(reg) dst,
1047            o1 = in(reg) dst.offset(ds),
1048            o2 = in(reg) dst.offset(2 * ds),
1049            o3 = in(reg) dst.offset(3 * ds),
1050            out("q0") _,
1051            out("q1") _,
1052            out("q2") _,
1053            out("q3") _,
1054            options(nostack),
1055        );
1056    }
1057}
1058
1059/// 4x4 transpose of 16-bit lanes on 32-bit arm, on 64-bit d registers: four
1060/// `vld1.16`, two `vtrn.16`, two `vtrn.32`, four `vst1.16`. Same NEON and
1061/// safety contract as [`transpose_4x4_neon_armv7_32`].
1062#[cfg(target_arch = "arm")]
1063#[inline(always)]
1064unsafe fn transpose_4x4_neon_armv7_16(
1065    src: *const u16,
1066    src_stride: isize,
1067    dst: *mut u16,
1068    dst_stride: usize,
1069) {
1070    use std::arch::asm;
1071    let ss = src_stride * 2;
1072    let ds = (dst_stride * 2) as isize;
1073    let src = src as *const u8;
1074    let dst = dst as *mut u8;
1075    unsafe {
1076        asm!(
1077            ".fpu neon",
1078            "vld1.16 {{d0}}, [{s0}]",
1079            "vld1.16 {{d1}}, [{s1}]",
1080            "vld1.16 {{d2}}, [{s2}]",
1081            "vld1.16 {{d3}}, [{s3}]",
1082            "vtrn.16 d0, d1",
1083            "vtrn.16 d2, d3",
1084            "vtrn.32 d0, d2",
1085            "vtrn.32 d1, d3",
1086            "vst1.16 {{d0}}, [{o0}]",
1087            "vst1.16 {{d1}}, [{o1}]",
1088            "vst1.16 {{d2}}, [{o2}]",
1089            "vst1.16 {{d3}}, [{o3}]",
1090            s0 = in(reg) src,
1091            s1 = in(reg) src.offset(ss),
1092            s2 = in(reg) src.offset(2 * ss),
1093            s3 = in(reg) src.offset(3 * ss),
1094            o0 = in(reg) dst,
1095            o1 = in(reg) dst.offset(ds),
1096            o2 = in(reg) dst.offset(2 * ds),
1097            o3 = in(reg) dst.offset(3 * ds),
1098            out("d0") _,
1099            out("d1") _,
1100            out("d2") _,
1101            out("d3") _,
1102            options(nostack),
1103        );
1104    }
1105}
1106
1107// K=4-inner packing writer (PackedI8K4 layout), fed in K-OUTER order (same feed
1108// as KOutWriter, used by the im2col patchers): for each k, all mn. Within a panel,
1109// element (k, local_mn) lands at (k/4)*r*4 + local_mn*4 + (k%4), so consecutive mn
1110// for a fixed k are stride-4 stores.
1111#[derive(Debug)]
1112pub struct KOut4Writer<'p, T>
1113where
1114    T: Copy + std::fmt::Debug,
1115{
1116    base: *mut T,
1117    r4: usize,        // r * 4
1118    panel_len: usize, // k_aligned * r
1119    panels: usize,
1120    panel_width: usize,
1121    last_panel_width: usize,
1122    kb: usize, // k / 4
1123    kr: usize, // k % 4
1124    panel: usize,
1125    local_mn: usize,
1126    _phantom: PhantomData<&'p T>,
1127}
1128
1129impl<'p, T> KOut4Writer<'p, T>
1130where
1131    T: Copy + std::fmt::Debug,
1132{
1133    pub fn new(base: *mut T, r: usize, panel_len: usize, mn: usize) -> KOut4Writer<'p, T> {
1134        let panels = mn.divceil(r).max(1);
1135        let last_panel_width = mn - (panels - 1) * r;
1136        KOut4Writer {
1137            base,
1138            r4: r * 4,
1139            panel_len,
1140            panels,
1141            panel_width: r,
1142            last_panel_width,
1143            kb: 0,
1144            kr: 0,
1145            panel: 0,
1146            local_mn: 0,
1147            _phantom: PhantomData,
1148        }
1149    }
1150    #[inline(always)]
1151    fn panel_width(&self) -> usize {
1152        if self.panel == self.panels - 1 { self.last_panel_width } else { self.panel_width }
1153    }
1154    #[inline(always)]
1155    fn advance(&mut self, by: usize) {
1156        self.local_mn += by;
1157        if self.local_mn >= self.panel_width() {
1158            self.local_mn = 0;
1159            self.panel += 1;
1160            if self.panel == self.panels {
1161                self.panel = 0;
1162                self.kr += 1;
1163                if self.kr == 4 {
1164                    self.kr = 0;
1165                    self.kb += 1;
1166                }
1167            }
1168        }
1169    }
1170}
1171
1172impl<T> PackingWriter<T> for KOut4Writer<'_, T>
1173where
1174    T: Copy + std::fmt::Debug,
1175{
1176    #[inline(always)]
1177    fn write(&mut self, t: T) {
1178        unsafe {
1179            let off = self.panel * self.panel_len + self.kb * self.r4 + self.local_mn * 4 + self.kr;
1180            *self.base.add(off) = t;
1181        }
1182        self.advance(1);
1183    }
1184
1185    #[inline]
1186    fn write_slice(&mut self, ts: &[T]) {
1187        let n = ts.len();
1188        if n == 0 {
1189            return;
1190        }
1191        let pw = self.panel_width();
1192        if self.local_mn + n <= pw {
1193            // Whole slice stays inside the current (panel, k): tight stride-4 store.
1194            unsafe {
1195                let mut d = self.base.add(
1196                    self.panel * self.panel_len + self.kb * self.r4 + self.local_mn * 4 + self.kr,
1197                );
1198                for &t in ts {
1199                    *d = t;
1200                    d = d.add(4);
1201                }
1202            }
1203            self.advance(n);
1204        } else {
1205            for &t in ts {
1206                self.write(t);
1207            }
1208        }
1209    }
1210}
1211
1212// K=4-inner packing for SDOT/relaxed-dot int8 matmul: 4 contiguous K per mn-lane.
1213// Layout: out[(k/4)*r*4 + m*4 + (k%4)] = src[m,k]. k_alignment=4. Matmul path uses
1214// pack_view; the conv im2col patchers feed write_with_k_outer in K-outer order.
1215#[derive(Clone, Debug, Hash, PartialEq, Eq)]
1216pub struct PackedI8K4 {
1217    pub r: usize,
1218    pub align: usize,
1219}
1220impl PackedI8K4 {
1221    pub fn new(r: usize) -> Self {
1222        PackedI8K4 { r, align: 16 }
1223    }
1224    fn panel(&self, k: usize) -> usize {
1225        (k.div_ceil(4) * 4) * self.r
1226    }
1227    pub fn single_panel_len(&self, k: usize) -> usize {
1228        self.panel(k)
1229    }
1230    pub fn len(&self, k: usize, mn: usize) -> usize {
1231        mn.divceil(self.r) * self.panel(k)
1232    }
1233    pub fn alignment(&self) -> usize {
1234        self.align
1235    }
1236    // One-pass K-outer writer for the conv im2col patchers (fed: for each k, all mn).
1237    pub fn write_with_k_outer<'p, T: Copy + std::fmt::Debug>(
1238        &self,
1239        pb: *mut T,
1240        k: usize,
1241        mn: usize,
1242    ) -> KOut4Writer<'p, T> {
1243        KOut4Writer::new(pb, self.r, self.panel(k), mn)
1244    }
1245    // K=4-inner pack from a (possibly strided) view: out[(k/4)*r*4 + m*4 + (k%4)] = src[m,k].
1246    pub fn pack_view(
1247        &self,
1248        t: &TensorView,
1249        k_axis: usize,
1250        mn_axis: usize,
1251    ) -> TractResult<Box<dyn MMMInputValue>> {
1252        ensure!(k_axis != mn_axis, "k_axis and mn_axis must differ (both are {k_axis})");
1253        let k = t.shape()[k_axis];
1254        let mn = t.shape()[mn_axis];
1255        let kp = k.div_ceil(4) * 4;
1256        let pl = kp * self.r;
1257        let panels = mn.div_ceil(self.r);
1258        let st = t.strides();
1259        let mut blob = unsafe { Blob::new_for_size_and_align(panels * pl, self.align) };
1260        blob.as_bytes_mut().fill(0);
1261        let (ks, ms) = (st[k_axis], st[mn_axis]);
1262        let kblocks = kp / 4;
1263        unsafe {
1264            let src = t.as_ptr_unchecked::<i8>();
1265            let dst = blob.as_mut_ptr() as *mut i8;
1266            for p in 0..panels {
1267                let pw = self.r.min(mn - p * self.r);
1268                let panel = dst.add(p * pl);
1269                let mn0 = (p * self.r) as isize;
1270                for kb in 0..kblocks {
1271                    for kr in 0..4 {
1272                        let kk = kb * 4 + kr;
1273                        if kk >= k {
1274                            break;
1275                        }
1276                        let srow = src.offset(kk as isize * ks + mn0 * ms);
1277                        let dcol = panel.add(kb * self.r * 4 + kr);
1278                        for lm in 0..pw {
1279                            *dcol.add(lm * 4) = *srow.offset(lm as isize * ms);
1280                        }
1281                    }
1282                }
1283            }
1284        }
1285        Ok(Box::new(EagerPackedInput {
1286            fact: PackedExoticFact { format: Box::new(self.clone()), mn: mn.to_dim(), k },
1287            packed: blob.into(),
1288            panel_bytes: pl,
1289            mn,
1290        }))
1291    }
1292}
1293impl std::fmt::Display for PackedI8K4 {
1294    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1295        write!(f, "I8K4[{}]", self.r)
1296    }
1297}
1298impl MMMInputFormat for PackedI8K4 {
1299    fn prepare_tensor(&self, t: &Tensor, k_axis: usize, mn_axis: usize) -> TractResult<Tensor> {
1300        Ok(PackedMatrixStorage::new(self.prepare_one(t, k_axis, mn_axis)?)
1301            .into_tensor(t.datum_type()))
1302    }
1303    fn prepare_one_view(
1304        &self,
1305        t: &TensorView,
1306        k_axis: usize,
1307        mn_axis: usize,
1308    ) -> TractResult<Box<dyn MMMInputValue>> {
1309        self.pack_view(t, k_axis, mn_axis)
1310    }
1311    fn precursor(&self) -> WeightType {
1312        WeightType::Plain(i8::datum_type())
1313    }
1314    fn r(&self) -> usize {
1315        self.r
1316    }
1317    fn k_alignment(&self) -> usize {
1318        4
1319    }
1320    fn merge_with<'o, 'a: 'o, 'b: 'o>(
1321        &'a self,
1322        o: &'b dyn MMMInputFormat,
1323    ) -> Option<&'o dyn MMMInputFormat> {
1324        o.downcast_ref::<PackedI8K4>().filter(|x| x.r == self.r).map(|_| self as _)
1325    }
1326    fn mem_size(&self, k: TDim, mn: TDim) -> TDim {
1327        mn.divceil(self.r) * self.panel(k.to_usize().unwrap_or(0))
1328    }
1329    fn extract_at_mn_f16(&self, _: &EagerPackedInput, _: usize, _: &mut [f16]) -> TractResult<()> {
1330        bail!("no f16 extract")
1331    }
1332    fn extract_at_mn_f32(&self, _: &EagerPackedInput, _: usize, _: &mut [f32]) -> TractResult<()> {
1333        bail!("no f32 extract")
1334    }
1335}
1336
1337pub trait Packing {
1338    fn packing(r: usize) -> PackedFormat;
1339}
1340
1341impl<D: Datum> Packing for D {
1342    fn packing(r: usize) -> PackedFormat {
1343        PackedFormat::new(Self::datum_type(), r, vector_size())
1344    }
1345}
1346
1347#[cfg(test)]
1348mod test {
1349    use std::ops::Range;
1350
1351    use proptest::prelude::*;
1352    use tract_data::internal::num_integer::Integer;
1353    use tract_data::internal::tract_ndarray::Zip;
1354    use tract_data::internal::*;
1355    use tract_ndarray::prelude::*;
1356
1357    #[derive(Debug)]
1358    struct PackProblem {
1359        k: usize,
1360        mn: usize,
1361        is_a: bool,
1362        r: usize,
1363        k_range: Range<usize>,
1364        mn_range: Range<usize>,
1365        align_panel: usize,
1366    }
1367
1368    impl PackProblem {
1369        fn input(&self) -> Array2<u32> {
1370            let shape = if self.is_a { (self.mn, self.k) } else { (self.k, self.mn) };
1371            let data = (0..(self.k * self.mn) as u32).collect();
1372            Array2::from_shape_vec(shape, data).unwrap()
1373        }
1374
1375        fn packer(&self) -> Array2<u32> {
1376            let panels = self.mn_range.len().divceil(self.r);
1377            let packer = super::PackedFormat::new(u32::datum_type(), self.r, self.align_panel)
1378                .with_end_padding_record(0);
1379            let input = self.input().into_tensor();
1380            let panel_len = packer.single_panel_len(self.k_range.len());
1381            let mut output =
1382                Tensor::zero::<u32>(&[packer.len(self.k_range.len(), self.mn_range.len())])
1383                    .unwrap();
1384            unsafe {
1385                packer.pack_segment(
1386                    output.view_mut(),
1387                    input.view(),
1388                    self.is_a as usize,
1389                    !self.is_a as usize,
1390                    self.k_range.clone(),
1391                    self.mn_range.clone(),
1392                )
1393            };
1394            output
1395                .into_plain_array::<u32>()
1396                .unwrap()
1397                .into_shape_with_order((panels, panel_len))
1398                .unwrap()
1399        }
1400
1401        fn reference(&self) -> Array2<u32> {
1402            let input = self.input();
1403            let panels = self.mn_range.len().divceil(self.r);
1404            let len = Integer::next_multiple_of(&(self.k_range.len() * self.r), &self.align_panel);
1405            Array2::from_shape_fn([panels, len], |(panel, z)| {
1406                let k = z / self.r;
1407                let x = z % self.r;
1408                let mn = panel * self.r + x + self.mn_range.start;
1409                let k = k + self.k_range.start;
1410                let coords = if self.is_a { (mn, k) } else { (k, mn) };
1411                *input.get(coords).unwrap_or(&0)
1412            })
1413        }
1414
1415        fn valid(&self) -> Array2<bool> {
1416            let panels = self.mn_range.len().divceil(self.r);
1417            let len = Integer::next_multiple_of(&(self.k_range.len() * self.r), &self.align_panel);
1418            Array2::from_shape_fn([panels, len], |(panel, z)| {
1419                let k = z / self.r;
1420                let x = z % self.r;
1421                let k = k + self.k_range.start;
1422                let mn = panel * self.r + x + self.mn_range.start;
1423                k < self.k_range.end.min(self.k) && mn < self.mn_range.end.min(self.mn)
1424            })
1425        }
1426
1427        fn check(&self) {
1428            let mut packer = self.packer();
1429            let mut reference = self.reference();
1430            let valid = self.valid();
1431            Zip::from(&mut packer).and(&valid).for_each(|p, v| *p = if *v { *p } else { -1 as _ });
1432            Zip::from(&mut reference)
1433                .and(&valid)
1434                .for_each(|p, v| *p = if *v { *p } else { -1 as _ });
1435            assert_eq!(packer, reference);
1436        }
1437    }
1438
1439    impl Arbitrary for PackProblem {
1440        type Parameters = ();
1441        type Strategy = BoxedStrategy<PackProblem>;
1442        fn arbitrary_with(_args: ()) -> Self::Strategy {
1443            (any::<bool>(), 1usize..9, 1usize..20, 1usize..20)
1444                .prop_flat_map(|(is_a, r, k, mn)| {
1445                    (
1446                        Just((is_a, r, k, mn)),
1447                        sub_range_strat(0..k),
1448                        sub_range_strat(0..mn),
1449                        1usize..5,
1450                    )
1451                })
1452                .prop_map(|((is_a, r, k, mn), k_range, mn_range, align_panel)| PackProblem {
1453                    k,
1454                    mn,
1455                    is_a,
1456                    r,
1457                    k_range,
1458                    mn_range,
1459                    align_panel,
1460                })
1461                .boxed()
1462        }
1463    }
1464
1465    fn sub_range_strat(range: Range<usize>) -> BoxedStrategy<Range<usize>> {
1466        (0..range.len())
1467            .prop_flat_map(|cropped| (Just(cropped), 0..=cropped))
1468            .prop_map(move |(cropped, left)| range.start + left..range.end - (cropped - left))
1469            .boxed()
1470    }
1471
1472    proptest::proptest! {
1473        #[test]
1474        fn prop(pb in any::<PackProblem>()) {
1475            pb.check();
1476        }
1477
1478        #[test]
1479        fn subrange_prop(_range in sub_range_strat(0..20)) {
1480        }
1481
1482    }
1483
1484    // ---- k-contiguous packing -----------------------------------------------
1485    //
1486    // A source whose k axis is contiguous (the shape an activation arrives in)
1487    // is packed with a blocked transpose, and the tiles are SIMD on some
1488    // targets. The result must stay byte-identical to feeding `KInWriter`
1489    // element by element, for every element width and for every panel width —
1490    // including the ones a 4x4 tile does not divide.
1491    #[derive(Debug, Clone)]
1492    struct PackKMajorProblem {
1493        k: usize,
1494        mn: usize,
1495        r: usize,
1496        align_panel: usize,
1497        k_range: Range<usize>,
1498        mn_range: Range<usize>,
1499    }
1500
1501    impl PackKMajorProblem {
1502        fn check<T: Datum + Copy + num_traits::Zero>(&self, value: impl Fn(usize, usize) -> T) {
1503            let input =
1504                Array2::from_shape_fn((self.mn, self.k), |(x, k)| value(x, k)).into_tensor();
1505            let packer = super::PackedFormat::new(T::datum_type(), self.r, self.align_panel);
1506            let len = packer.len(self.k_range.len(), self.mn_range.len());
1507
1508            let mut packed = Tensor::zero::<T>(&[len]).unwrap();
1509            unsafe {
1510                // [mn, k]: k_axis 1, mn_axis 0, so k_stride is 1.
1511                packer.pack_segment(
1512                    packed.view_mut(),
1513                    input.view(),
1514                    1,
1515                    0,
1516                    self.k_range.clone(),
1517                    self.mn_range.clone(),
1518                )
1519            };
1520
1521            let mut reference = Tensor::zero::<T>(&[len]).unwrap();
1522            let input = input.to_plain_array_view::<T>().unwrap();
1523            unsafe {
1524                let mut writer = packer.write_with_k_inner(
1525                    reference.as_ptr_mut_unchecked::<T>(),
1526                    self.k_range.len(),
1527                    self.mn,
1528                );
1529                for x in self.mn_range.start..self.mn_range.end.min(self.mn) {
1530                    for k in self.k_range.clone() {
1531                        super::PackingWriter::write(&mut writer, input[[x, k]]);
1532                    }
1533                }
1534            }
1535
1536            assert_eq!(packed, reference, "{self:?} for {:?}", T::datum_type());
1537        }
1538
1539        fn check_all_widths(&self) {
1540            self.check(|x, k| (x * 41 + k * 7) as u32);
1541            self.check(|x, k| f16::from_f32((x * 41 + k * 7) as f32));
1542            self.check(|x, k| (x * 41 + k * 7) as u8);
1543        }
1544    }
1545
1546    impl Arbitrary for PackKMajorProblem {
1547        type Parameters = ();
1548        type Strategy = BoxedStrategy<PackKMajorProblem>;
1549        fn arbitrary_with(_: ()) -> Self::Strategy {
1550            // r covers the panel widths of the real f32/f16 kernels plus the
1551            // ones smaller than a tile.
1552            (
1553                prop::sample::select(vec![1usize, 2, 3, 4, 5, 8, 12, 16, 24, 32]),
1554                1usize..40,
1555                1usize..40,
1556            )
1557                .prop_flat_map(|(r, k, mn)| {
1558                    (Just((r, k, mn)), 1usize..5, sub_range_strat(0..k), sub_range_strat(0..mn))
1559                })
1560                .prop_map(|((r, k, mn), align_panel, k_range, mn_range)| PackKMajorProblem {
1561                    k,
1562                    mn,
1563                    r,
1564                    align_panel,
1565                    k_range,
1566                    mn_range,
1567                })
1568                .boxed()
1569        }
1570    }
1571
1572    proptest::proptest! {
1573        #[test]
1574        fn pack_k_major_prop(pb in any::<PackKMajorProblem>()) {
1575            pb.check_all_widths();
1576        }
1577    }
1578
1579    fn k_major(k: usize, mn: usize, r: usize) -> PackKMajorProblem {
1580        PackKMajorProblem { k, mn, r, align_panel: 1, k_range: 0..k, mn_range: 0..mn }
1581    }
1582
1583    #[test]
1584    fn k_major_exact_tiles() {
1585        k_major(4, 4, 4).check_all_widths();
1586        k_major(768, 256, 8).check_all_widths();
1587        k_major(16, 32, 16).check_all_widths();
1588    }
1589
1590    #[test]
1591    fn k_major_tails() {
1592        // k % 4, panel width % 4, and both at once.
1593        for k in [1, 2, 3, 5, 7] {
1594            k_major(k, 8, 8).check_all_widths();
1595            k_major(k, 7, 8).check_all_widths();
1596        }
1597        k_major(9, 6, 12).check_all_widths();
1598        k_major(9, 30, 12).check_all_widths();
1599    }
1600
1601    #[test]
1602    fn k_major_tile_over_threshold() {
1603        // Blocks past the armv7 size gate, so the NEON tile runs (not just the
1604        // scalar tail the proptest's small blocks take there) while still
1605        // hitting the k, panel-width, and narrow-last-panel tails.
1606        k_major(129, 41, 8).check_all_widths();
1607        k_major(160, 26, 16).check_all_widths();
1608        k_major(130, 44, 12).check_all_widths();
1609        k_major(140, 30, 8).check_all_widths();
1610    }
1611
1612    #[test]
1613    fn k_major_narrower_than_a_tile() {
1614        for r in [1, 2, 3] {
1615            k_major(9, 7, r).check_all_widths();
1616        }
1617    }
1618
1619    #[test]
1620    fn k_major_segments() {
1621        // A cropped k_range must still land at panel offset 0.
1622        PackKMajorProblem { k: 20, mn: 20, r: 8, align_panel: 1, k_range: 3..17, mn_range: 0..20 }
1623            .check_all_widths();
1624        PackKMajorProblem { k: 20, mn: 20, r: 8, align_panel: 1, k_range: 0..20, mn_range: 4..12 }
1625            .check_all_widths();
1626        // mn_range reaching past mn: the invalid columns are left untouched.
1627        PackKMajorProblem { k: 20, mn: 20, r: 8, align_panel: 1, k_range: 0..20, mn_range: 16..24 }
1628            .check_all_widths();
1629    }
1630
1631    // ---- PackedI8K4 (K=4-inner SMOPA/SDOT layout) dedicated tests ----------
1632    //
1633    // PackedI8K4 has two independent producers that MUST agree byte-for-byte:
1634    //   * `pack_view`           — the matmul path, reads a (possibly strided)
1635    //                             TensorView and packs in one shot.
1636    //   * `write_with_k_outer`  — the conv/im2col path, fed element-by-element
1637    //                             in K-OUTER order (for each k, all mn).
1638    // Both must equal the canonical layout
1639    //     out[panel*pl + (k/4)*r*4 + local_mn*4 + (k%4)] = src[k, panel*r+local_mn]
1640    // with pl = ceil(K/4)*4 * r, and every padding byte (K%4 tail, partial last
1641    // mn panel) left at zero.
1642    #[derive(Debug, Clone)]
1643    struct PackI8K4Problem {
1644        k: usize,
1645        mn: usize,
1646        r: usize,
1647        // false: input tensor is [k, mn] (k_axis=0, mn_axis=1) — contiguous read.
1648        // true : input tensor is [mn, k] (k_axis=1, mn_axis=0) — strided read,
1649        //        mirroring how the "A" operand is fed.
1650        is_a: bool,
1651    }
1652
1653    impl PackI8K4Problem {
1654        // Canonical logical matrix, always indexed [k, mn].
1655        fn logical(&self) -> Array2<i8> {
1656            Array2::from_shape_fn((self.k, self.mn), |(kk, m)| {
1657                (kk.wrapping_mul(31).wrapping_add(m.wrapping_mul(17)).wrapping_add(1)) as i8
1658            })
1659        }
1660
1661        fn panel_len(&self) -> usize {
1662            (self.k.div_ceil(4) * 4) * self.r
1663        }
1664
1665        // The layout every producer must reproduce.
1666        fn reference(&self) -> Vec<i8> {
1667            let logical = self.logical();
1668            let r = self.r;
1669            let pl = self.panel_len();
1670            let panels = self.mn.div_ceil(r);
1671            let mut out = vec![0i8; panels * pl];
1672            for p in 0..panels {
1673                let pw = r.min(self.mn - p * r);
1674                for kk in 0..self.k {
1675                    for lm in 0..pw {
1676                        let m = p * r + lm;
1677                        let off = p * pl + (kk / 4) * r * 4 + lm * 4 + (kk % 4);
1678                        out[off] = logical[[kk, m]];
1679                    }
1680                }
1681            }
1682            out
1683        }
1684
1685        // The matmul path: pack a TensorView, then read it back panel by panel.
1686        fn pack_view_bytes(&self) -> Vec<i8> {
1687            let logical = self.logical();
1688            let packer = super::PackedI8K4::new(self.r);
1689            let (tensor, k_axis, mn_axis) = if self.is_a {
1690                // [mn, k] with entry [m, kk] == logical[kk, m]; reads are strided.
1691                let a = Array2::from_shape_fn((self.mn, self.k), |(m, kk)| logical[[kk, m]]);
1692                (a.into_tensor(), 1usize, 0usize)
1693            } else {
1694                (logical.clone().into_tensor(), 0usize, 1usize)
1695            };
1696            let packed = packer.pack_view(&tensor.view(), k_axis, mn_axis).unwrap();
1697            let pl = self.panel_len();
1698            let panels = self.mn.div_ceil(self.r);
1699            assert_eq!(packed.panels_count(), panels);
1700            assert_eq!(packed.k(), self.k);
1701            assert_eq!(packed.mn(), self.mn);
1702            let mut out = vec![0i8; panels * pl];
1703            unsafe {
1704                for p in 0..panels {
1705                    let ptr = packed.panel_bytes(p, None).unwrap() as *const i8;
1706                    std::ptr::copy_nonoverlapping(ptr, out.as_mut_ptr().add(p * pl), pl);
1707                }
1708            }
1709            out
1710        }
1711
1712        // The conv path: feed the writer in K-outer order (for each k, all mn).
1713        fn writer_bytes(&self) -> Vec<i8> {
1714            let logical = self.logical();
1715            let packer = super::PackedI8K4::new(self.r);
1716            let total = packer.len(self.k, self.mn);
1717            assert_eq!(total, self.mn.div_ceil(self.r) * self.panel_len());
1718            let mut buf = vec![0i8; total];
1719            {
1720                let mut w = packer.write_with_k_outer(buf.as_mut_ptr(), self.k, self.mn);
1721                for kk in 0..self.k {
1722                    for m in 0..self.mn {
1723                        super::PackingWriter::write(&mut w, logical[[kk, m]]);
1724                    }
1725                }
1726            }
1727            buf
1728        }
1729
1730        fn check(&self) {
1731            let reference = self.reference();
1732            assert_eq!(
1733                self.pack_view_bytes(),
1734                reference,
1735                "pack_view disagrees with reference for {self:?}"
1736            );
1737            assert_eq!(
1738                self.writer_bytes(),
1739                reference,
1740                "write_with_k_outer disagrees with reference for {self:?}"
1741            );
1742        }
1743    }
1744
1745    impl Arbitrary for PackI8K4Problem {
1746        type Parameters = ();
1747        type Strategy = BoxedStrategy<PackI8K4Problem>;
1748        fn arbitrary_with(_: ()) -> Self::Strategy {
1749            // r is the tile width used by the int8 kernels (SMOPA 32, SDOT 8, ...).
1750            (any::<bool>(), prop::sample::select(vec![4usize, 8, 16, 32]), 1usize..40, 1usize..40)
1751                .prop_map(|(is_a, r, k, mn)| PackI8K4Problem { k, mn, r, is_a })
1752                .boxed()
1753        }
1754    }
1755
1756    proptest::proptest! {
1757        #[test]
1758        fn pack_i8k4_prop(pb in any::<PackI8K4Problem>()) {
1759            pb.check();
1760        }
1761    }
1762
1763    fn k4(k: usize, mn: usize, r: usize, is_a: bool) -> PackI8K4Problem {
1764        PackI8K4Problem { k, mn, r, is_a }
1765    }
1766
1767    #[test]
1768    fn i8k4_smallest() {
1769        k4(1, 1, 4, false).check();
1770        k4(1, 1, 4, true).check();
1771    }
1772
1773    #[test]
1774    fn i8k4_exact_tile() {
1775        // K and mn land exactly on the 4 / r boundaries: no padding anywhere.
1776        k4(4, 4, 4, false).check();
1777        k4(8, 32, 32, false).check();
1778        k4(8, 32, 32, true).check();
1779    }
1780
1781    #[test]
1782    fn i8k4_k_not_multiple_of_4() {
1783        // K%4 tail must be zero-padded inside each panel.
1784        for k in [1, 2, 3, 5, 6, 7, 9] {
1785            k4(k, 4, 4, false).check();
1786            k4(k, 7, 8, true).check();
1787        }
1788    }
1789
1790    #[test]
1791    fn i8k4_partial_last_panel() {
1792        // mn not a multiple of r: last panel is narrower, tail lanes are zero.
1793        k4(5, 7, 4, false).check();
1794        k4(5, 7, 4, true).check();
1795        k4(4, 33, 32, false).check();
1796        k4(4, 33, 32, true).check();
1797        k4(3, 1, 32, false).check();
1798    }
1799
1800    #[test]
1801    fn i8k4_single_wide_tile() {
1802        // One narrow panel inside a wide (r=32) tile.
1803        k4(7, 1, 32, false).check();
1804        k4(7, 5, 16, true).check();
1805    }
1806
1807    #[test]
1808    fn i8k4_many_panels() {
1809        k4(13, 100, 8, false).check();
1810        k4(13, 100, 8, true).check();
1811        k4(17, 65, 16, false).check();
1812    }
1813
1814    #[test]
1815    fn simple_b_1() {
1816        PackProblem {
1817            k: 2,
1818            mn: 1,
1819            is_a: false,
1820            r: 1,
1821            k_range: 0..2,
1822            mn_range: 0..1,
1823            align_panel: 1,
1824        }
1825        .check();
1826    }
1827
1828    #[test]
1829    fn simple_b_2() {
1830        PackProblem {
1831            k: 2,
1832            mn: 2,
1833            is_a: false,
1834            r: 1,
1835            k_range: 0..2,
1836            mn_range: 0..2,
1837            align_panel: 1,
1838        }
1839        .check()
1840    }
1841
1842    #[test]
1843    fn simple_b_3() {
1844        PackProblem {
1845            k: 2,
1846            mn: 1,
1847            is_a: false,
1848            r: 4,
1849            k_range: 0..2,
1850            mn_range: 0..1,
1851            align_panel: 1,
1852        }
1853        .check();
1854    }
1855
1856    #[test]
1857    fn simple_b_4() {
1858        PackProblem {
1859            k: 1,
1860            mn: 3,
1861            is_a: false,
1862            r: 2,
1863            k_range: 0..1,
1864            mn_range: 0..3,
1865            align_panel: 1,
1866        }
1867        .check();
1868    }
1869
1870    #[test]
1871    fn simple_a_1() {
1872        PackProblem {
1873            k: 2,
1874            mn: 2,
1875            is_a: true,
1876            r: 1,
1877            k_range: 0..2,
1878            mn_range: 0..2,
1879            align_panel: 1,
1880        }
1881        .check();
1882    }
1883
1884    #[test]
1885    fn simple_a_2() {
1886        PackProblem {
1887            k: 2,
1888            mn: 3,
1889            is_a: true,
1890            r: 2,
1891            k_range: 0..2,
1892            mn_range: 0..3,
1893            align_panel: 1,
1894        }
1895        .check();
1896    }
1897
1898    #[test]
1899    fn range_k_0() {
1900        PackProblem {
1901            k: 2,
1902            mn: 1,
1903            is_a: false,
1904            r: 1,
1905            k_range: 1..2,
1906            mn_range: 0..1,
1907            align_panel: 1,
1908        }
1909        .check();
1910    }
1911
1912    #[test]
1913    fn range_k_1() {
1914        PackProblem {
1915            k: 2,
1916            mn: 2,
1917            is_a: false,
1918            r: 1,
1919            k_range: 0..2,
1920            mn_range: 0..1,
1921            align_panel: 1,
1922        }
1923        .check();
1924    }
1925
1926    #[test]
1927    fn range_k_2() {
1928        PackProblem {
1929            k: 2,
1930            mn: 1,
1931            is_a: false,
1932            r: 6,
1933            k_range: 1..2,
1934            mn_range: 0..1,
1935            align_panel: 1,
1936        }
1937        .check();
1938    }
1939
1940    #[test]
1941    fn range_mn_0() {
1942        PackProblem {
1943            k: 1,
1944            mn: 2,
1945            is_a: false,
1946            r: 2,
1947            k_range: 0..1,
1948            mn_range: 0..1,
1949            align_panel: 1,
1950        }
1951        .check();
1952    }
1953
1954    #[test]
1955    fn range_b_4() {
1956        PackProblem {
1957            k: 1,
1958            mn: 2,
1959            is_a: false,
1960            r: 6,
1961            k_range: 0..1,
1962            mn_range: 1..2,
1963            align_panel: 1,
1964        }
1965        .check();
1966    }
1967
1968    #[test]
1969    fn range_b_5() {
1970        PackProblem {
1971            k: 1,
1972            mn: 7,
1973            is_a: false,
1974            r: 6,
1975            k_range: 0..1,
1976            mn_range: 1..7,
1977            align_panel: 1,
1978        }
1979        .check();
1980    }
1981
1982    #[test]
1983    fn align_a_1() {
1984        PackProblem {
1985            k: 2,
1986            mn: 2,
1987            is_a: true,
1988            r: 1,
1989            k_range: 0..1,
1990            mn_range: 0..2,
1991            align_panel: 2,
1992        }
1993        .check();
1994    }
1995
1996    #[test]
1997    fn align_b_1() {
1998        PackProblem {
1999            k: 1,
2000            mn: 1,
2001            is_a: false,
2002            r: 1,
2003            k_range: 0..1,
2004            mn_range: 0..1,
2005            align_panel: 2,
2006        }
2007        .check();
2008    }
2009
2010    #[test]
2011    fn align_b_2() {
2012        PackProblem {
2013            k: 3,
2014            mn: 1,
2015            is_a: false,
2016            r: 1,
2017            k_range: 0..3,
2018            mn_range: 0..1,
2019            align_panel: 2,
2020        }
2021        .check();
2022    }
2023
2024    #[test]
2025    fn align_b_3() {
2026        PackProblem {
2027            k: 1,
2028            mn: 1,
2029            is_a: false,
2030            r: 3,
2031            k_range: 0..1,
2032            mn_range: 0..1,
2033            align_panel: 2,
2034        }
2035        .check();
2036    }
2037
2038    #[test]
2039    fn align_b_4() {
2040        PackProblem {
2041            k: 2,
2042            mn: 1,
2043            is_a: false,
2044            r: 1,
2045            k_range: 0..1,
2046            mn_range: 0..1,
2047            align_panel: 2,
2048        }
2049        .check();
2050    }
2051
2052    #[test]
2053    fn align_b_5() {
2054        PackProblem {
2055            k: 1,
2056            mn: 5,
2057            is_a: false,
2058            r: 4,
2059            k_range: 0..1,
2060            mn_range: 0..5,
2061            align_panel: 3,
2062        }
2063        .check();
2064    }
2065
2066    #[cfg(feature = "multithread-mm")]
2067    #[test]
2068    fn packing_work_threshold() {
2069        use super::{PACKING_MIN_CHUNK_BYTES, packing_has_enough_work};
2070
2071        for threads in [2, 4, 8] {
2072            for panels in [2, 3, 4 * threads - 1, 4 * threads, 4 * threads + 1, 1001] {
2073                let boundary = (4 * threads).min(panels) * PACKING_MIN_CHUNK_BYTES;
2074                assert!(!packing_has_enough_work(panels, boundary - 1, threads));
2075                assert!(packing_has_enough_work(panels, boundary, threads));
2076                assert!(packing_has_enough_work(panels, boundary + 1, threads));
2077            }
2078        }
2079        for threads in [0, 1, 4] {
2080            assert!(!packing_has_enough_work(0, usize::MAX, threads));
2081            assert!(!packing_has_enough_work(1, usize::MAX, threads));
2082        }
2083        assert!(!packing_has_enough_work(1001, usize::MAX, 1));
2084    }
2085
2086    #[cfg(feature = "multithread-mm")]
2087    #[cfg(not(target_arch = "wasm32"))]
2088    #[test]
2089    fn multithreaded_pack_matches_serial() {
2090        use crate::frame::mmm::EagerPackedInput;
2091        use crate::multithread::{Executor, multithread_tract_scope};
2092        // Panel tails (end padding record, alignment) are never written: compare the k * r values.
2093        let pack = |format: &super::PackedFormat, t: &Tensor, k_axis: usize, mn_axis: usize| {
2094            let k = t.shape()[k_axis];
2095            let packed = format.pack_tensor_view(&t.view(), k_axis, mn_axis).unwrap();
2096            let packed = packed.downcast_ref::<EagerPackedInput>().unwrap();
2097            let values = k * format.r * t.datum_type().size_of();
2098            packed
2099                .packed
2100                .as_bytes()
2101                .chunks(packed.panel_bytes)
2102                .flat_map(|panel| panel[..values].to_vec())
2103                .collect::<Vec<u8>>()
2104        };
2105        let executors = [Executor::multithread(1), Executor::multithread(4)];
2106        for dt in [DatumType::U8, DatumType::F16, DatumType::F32] {
2107            let boundary_mn = 16 * super::PACKING_MIN_CHUNK_BYTES / (128 * dt.size_of());
2108            for (k, mn) in [
2109                (64, 600),
2110                (37, 1001),
2111                (9, 5000),
2112                (16384, 9),
2113                (128, boundary_mn - 1),
2114                (128, boundary_mn),
2115                (128, boundary_mn + 1),
2116            ] {
2117                for r in [4, 5, 8, 16, 24, 32] {
2118                    let format = super::PackedFormat::new(dt, r, 16);
2119                    for (shape, k_axis, mn_axis) in
2120                        [(vec![k, mn], 0, 1), (vec![mn, k], 1, 0), (vec![k, mn, 2], 0, 1)]
2121                    {
2122                        let data: Vec<f32> = (0..shape.iter().product::<usize>())
2123                            .map(|i| (i % 257) as f32 * 0.5)
2124                            .collect();
2125                        let t = Tensor::from_shape(&shape, &data).unwrap();
2126                        let t = t.cast_to_dt(dt).unwrap().into_owned();
2127                        let serial = multithread_tract_scope(Executor::SingleThread, || {
2128                            pack(&format, &t, k_axis, mn_axis)
2129                        });
2130                        for executor in &executors {
2131                            let parallel = multithread_tract_scope(executor.clone(), || {
2132                                pack(&format, &t, k_axis, mn_axis)
2133                            });
2134                            assert_eq!(
2135                                serial, parallel,
2136                                "k {k} mn {mn} r {r} {dt:?} shape {shape:?}"
2137                            );
2138                        }
2139                    }
2140                }
2141            }
2142        }
2143    }
2144}