Skip to main content

tract_linalg/frame/mmm/
scratch.rs

1use super::{FusedKerSpec, FusedSpec, MatMatMulKer, OutputStoreKer};
2use crate::{BinOp, LADatum};
3use downcast_rs::{Downcast, impl_downcast};
4use std::cell::RefCell;
5use std::fmt::Debug;
6use std::sync::atomic::AtomicUsize;
7use tract_data::internal::num_integer::Integer;
8use tract_data::internal::*;
9
10static GENERATION: AtomicUsize = AtomicUsize::new(1);
11
12thread_local! {
13    static TLS: RefCell<TLSScratch> = Default::default();
14}
15
16#[derive(Default, Debug)]
17pub(crate) struct TLSScratch {
18    generation: usize,
19    blob: Blob,
20    ker_specs_16: Vec<FusedKerSpec<f16>>,
21    ker_specs_32: Vec<FusedKerSpec<f32>>,
22    ker_specs_64: Vec<FusedKerSpec<f64>>,
23}
24
25impl TLSScratch {
26    #[allow(unknown_lints, clippy::missing_transmute_annotations)]
27    fn ker_specs<TI: LADatum>(&mut self) -> &mut Vec<FusedKerSpec<TI>> {
28        unsafe {
29            if TI::datum_type() == f32::datum_type() || TI::datum_type() == i32::datum_type() {
30                std::mem::transmute(&mut self.ker_specs_32)
31            } else if TI::datum_type() == f16::datum_type() {
32                std::mem::transmute(&mut self.ker_specs_16)
33            } else if TI::datum_type() == f64::datum_type() {
34                std::mem::transmute(&mut self.ker_specs_64)
35            } else {
36                todo!();
37            }
38        }
39    }
40
41    fn sync<TI: LADatum>(&mut self, scratch: &ScratchSpaceImpl<TI>) {
42        if self.generation == scratch.generation {
43            return;
44        }
45        let ker_specs = self.ker_specs::<TI>();
46        ker_specs.clear();
47        ker_specs.extend_from_slice(&scratch.ker_specs);
48
49        unsafe {
50            self.blob.ensure_size_and_align(scratch.blob_size, scratch.blob_align);
51
52            for LocDependent { loc, ker_spec, .. } in &scratch.loc_dependent {
53                #[allow(clippy::single_match)]
54                if matches!(scratch.ker_specs[*ker_spec], FusedKerSpec::AddMatMul { .. }) {
55                    let scratch = &mut *(self.blob.as_ptr().add(*loc) as *mut AddMatMulTemp);
56                    scratch.panel_a_id = usize::MAX;
57                    scratch.panel_b_id = usize::MAX;
58                };
59            }
60        }
61        self.generation = scratch.generation;
62    }
63}
64
65pub trait ScratchSpace: Downcast + Send {}
66impl_downcast!(ScratchSpace);
67
68#[derive(Debug, Default)]
69pub struct ScratchSpaceImpl<TI: LADatum> {
70    generation: usize,
71    blob_size: usize,
72    blob_align: usize,
73    ker_specs: Vec<FusedKerSpec<TI>>,
74    loc_dependent: TVec<LocDependent>,
75    valid_down_tiles: usize,
76    remnant_down: usize,
77    valid_right_tiles: usize,
78    remnant_right: usize,
79}
80
81#[derive(Debug, new)]
82struct LocDependent {
83    spec: usize,
84    ker_spec: usize,
85    // offset for the location dependent structure
86    loc: usize,
87    // offset of its associated dynamic-size buffers
88    buffer_a: Option<usize>,
89    buffer_b: Option<usize>,
90}
91
92impl<TI: LADatum> ScratchSpace for ScratchSpaceImpl<TI> {}
93unsafe impl<TI: LADatum> Send for ScratchSpaceImpl<TI> {}
94
95#[derive(Debug)]
96struct AddMatMulTemp {
97    ptr_a: *const u8,
98    panel_a_id: usize,
99    ptr_b: *const u8,
100    panel_b_id: usize,
101}
102
103/// True when this border tile can store its live rows straight into C.
104///
105/// Only `sme_mmm_f32_32x32` understands the packing hint, and only for a
106/// full N panel whose columns are contiguous. The tile must also contain a
107/// matmul: the hint travels in that op, and without it the kernel stores
108/// all 32 rows. A count of 16 or less also skips the bottom-tile FMOPAs;
109/// 17..31 only shortens the store.
110fn partial_m_direct<TI: LADatum>(
111    ker: &impl MatMatMulKer<Acc = TI>,
112    specs: &[FusedSpec],
113    m_remnant: usize,
114    n_remnant: usize,
115) -> bool {
116    if ker.name() != "sme_mmm_f32_32x32" {
117        return false;
118    }
119    if m_remnant == 0
120        || m_remnant >= ker.mr()
121        || n_remnant != ker.nr()
122        || !ker.stores_row_major_tile()
123    {
124        return false;
125    }
126    let mut saw_store = false;
127    let mut saw_matmul = false;
128    for spec in specs {
129        match spec {
130            FusedSpec::Store(store) => {
131                saw_store = true;
132                if store.col_byte_stride != store.item_size as isize || store.item_size != 4 {
133                    return false;
134                }
135            }
136            FusedSpec::AddMatMul { .. } => saw_matmul = true,
137            _ => {}
138        }
139    }
140    saw_store && saw_matmul
141}
142
143impl<TI: LADatum> ScratchSpaceImpl<TI> {
144    pub unsafe fn prepare(
145        &mut self,
146        ker: &impl MatMatMulKer<Acc = TI>,
147        m: usize,
148        n: usize,
149        specs: &[FusedSpec],
150    ) -> TractResult<()> {
151        use FusedKerSpec as FKS;
152        use FusedSpec as FS;
153        self.ker_specs.clear();
154        self.loc_dependent.clear();
155        self.ker_specs.reserve(specs.len() + 2);
156        self.ker_specs.push(FusedKerSpec::Clear);
157        self.valid_down_tiles = m / ker.mr();
158        self.remnant_down = m % ker.mr();
159        self.valid_right_tiles = n / ker.nr();
160        self.remnant_right = n % ker.nr();
161        let mut offset = 0;
162        let mut align = std::mem::size_of::<*const ()>();
163        fn ld(spec: usize, uspec: usize, loc: usize) -> LocDependent {
164            LocDependent { spec, ker_spec: uspec, loc, buffer_a: None, buffer_b: None }
165        }
166        for (ix, spec) in specs.iter().enumerate() {
167            offset = offset.next_multiple_of(&align);
168            let ker_spec = match spec {
169                FS::BinScalar(t, op) => match op {
170                    BinOp::Min => FKS::ScalarMin(*t.try_as_plain_ram()?.to_scalar()?),
171                    BinOp::Max => FKS::ScalarMax(*t.try_as_plain_ram()?.to_scalar()?),
172                    BinOp::Mul => FKS::ScalarMul(*t.try_as_plain_ram()?.to_scalar()?),
173                    BinOp::Add => FKS::ScalarAdd(*t.try_as_plain_ram()?.to_scalar()?),
174                    BinOp::Sub => FKS::ScalarSub(*t.try_as_plain_ram()?.to_scalar()?),
175                    BinOp::SubF => FKS::ScalarSubF(*t.try_as_plain_ram()?.to_scalar()?),
176                },
177                FS::ShiftLeft(s) => FKS::ShiftLeft(*s),
178                FS::RoundingShiftRight(s, rp) => FKS::RoundingShiftRight(*s, *rp),
179                FS::QScale(s, rp, m) => FKS::QScale(*s, *rp, *m),
180                FS::BinPerRow(_, _) => {
181                    self.loc_dependent.push(ld(ix, self.ker_specs.len(), offset));
182                    offset += TI::datum_type().size_of() * ker.mr();
183                    FusedKerSpec::Done
184                }
185                FS::BinPerCol(_, _) => {
186                    self.loc_dependent.push(ld(ix, self.ker_specs.len(), offset));
187                    offset += TI::datum_type().size_of() * ker.nr();
188                    FusedKerSpec::Done
189                }
190                FS::AddRowColProducts(_, _) => {
191                    self.loc_dependent.push(ld(ix, self.ker_specs.len(), offset));
192                    offset += TI::datum_type().size_of() * (ker.mr() + ker.nr());
193                    FusedKerSpec::Done
194                }
195                FS::AddUnicast(_) => {
196                    self.loc_dependent.push(ld(ix, self.ker_specs.len(), offset));
197                    offset += TI::datum_type().size_of() * ker.mr() * ker.nr();
198                    FusedKerSpec::Done
199                }
200                FS::Store(store) => {
201                    // Only worth a row-major tile when the destination is itself
202                    // n-contiguous: set_from_tile then copies without transposing
203                    // and the kernel's aligned bulk-store path applies.
204                    let row_major = ker.stores_row_major_tile()
205                        && store.col_byte_stride == store.item_size as isize;
206                    let tile_bytes = if row_major {
207                        // 128-align the row-major store tile, with rows padded to
208                        // 128 bytes, so the kernel can hit its aligned bulk-store
209                        // path (e.g. Apple AMX stz-direct).
210                        align = align.lcm(&128);
211                        offset = Integer::next_multiple_of(&offset, &128);
212                        Integer::next_multiple_of(&(store.item_size * ker.nr()), &128) * ker.mr()
213                    } else {
214                        store.item_size * ker.mr() * ker.nr()
215                    };
216                    self.loc_dependent.push(ld(ix, self.ker_specs.len(), offset));
217                    offset += tile_bytes;
218                    FusedKerSpec::Done
219                }
220                FS::LeakyRelu(t) => FKS::LeakyRelu(*t.try_as_plain_ram()?.to_scalar()?),
221                FS::AddMatMul { a, b, packing } => {
222                    let mut ld = ld(ix, self.ker_specs.len(), offset);
223                    offset += std::mem::size_of::<AddMatMulTemp>();
224                    if let Some(tmp) = a.scratch_panel_buffer_layout() {
225                        align = tmp.align().lcm(&align);
226                        offset = Integer::next_multiple_of(&offset, &tmp.align());
227                        ld.buffer_a = Some(offset);
228                        offset += tmp.size();
229                    }
230                    if let Some(tmp) = b.scratch_panel_buffer_layout() {
231                        align = tmp.align().lcm(&align);
232                        offset = Integer::next_multiple_of(&offset, &tmp.align());
233                        ld.buffer_b = Some(offset);
234                        offset += tmp.size();
235                    }
236                    self.loc_dependent.push(ld);
237                    FusedKerSpec::AddMatMul {
238                        k: 0,
239                        pa: std::ptr::null(),
240                        pb: std::ptr::null(),
241                        packing: *packing,
242                    }
243                }
244            };
245            self.ker_specs.push(ker_spec);
246        }
247        self.ker_specs.push(FKS::Done);
248        self.blob_size = offset;
249        self.blob_align = align;
250
251        self.generation = GENERATION.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
252        Ok(())
253    }
254
255    pub unsafe fn run(
256        &self,
257        ker: &impl MatMatMulKer<Acc = TI>,
258        specs: &[FusedSpec],
259        down: usize,
260        right: usize,
261    ) -> TractResult<()> {
262        // Per-tile entry: enter the TLS scope (does sync once) then run a single
263        // tile. Single-threaded callers should prefer `run_in_tls_scope`+
264        // `run_one_tile` to amortise the TLS borrow + sync over many tiles.
265        unsafe {
266            self.run_in_tls_scope(|this, tls| this.run_one_tile(ker, specs, tls, down, right))
267        }
268    }
269
270    /// Borrow the per-thread scratch blob for a single MMM call and `sync` it
271    /// once. The closure is invoked once with a mutable reference to the TLS
272    /// scratch and to `self`. Used by single-threaded matmul drivers to avoid
273    /// re-entering TLS / re-running `sync` per tile.
274    pub(crate) unsafe fn run_in_tls_scope<F, R>(&self, f: F) -> R
275    where
276        F: FnOnce(&Self, &mut TLSScratch) -> R,
277    {
278        TLS.with_borrow_mut(|tls| {
279            tls.sync(self);
280            f(self, tls)
281        })
282    }
283
284    /// Run a single tile against an already-borrowed TLS scratch. Caller is
285    /// responsible for entering `run_in_tls_scope` first (so `sync` has run).
286    #[inline(always)]
287    pub(crate) unsafe fn run_one_tile(
288        &self,
289        ker: &impl MatMatMulKer<Acc = TI>,
290        specs: &[FusedSpec],
291        tls: &mut TLSScratch,
292        down: usize,
293        right: usize,
294    ) -> TractResult<()> {
295        unsafe {
296            if down < self.valid_down_tiles && right < self.valid_right_tiles {
297                self.for_valid_tile(ker, specs, tls, down, right)?;
298                let err = ker.kernel(tls.ker_specs());
299                ensure!(err == 0, "Kernel {} returned error {err}", ker.name());
300            } else {
301                let remnant_down =
302                    if down < self.valid_down_tiles { ker.mr() } else { self.remnant_down };
303                let remnant_right =
304                    if right < self.valid_right_tiles { ker.nr() } else { self.remnant_right };
305                let direct = self.for_border_tile(
306                    ker,
307                    specs,
308                    tls,
309                    down,
310                    right,
311                    remnant_down,
312                    remnant_right,
313                )?;
314                let err = ker.kernel(tls.ker_specs());
315                ensure!(err == 0, "Kernel {} returned error {err}", ker.name());
316                if !direct {
317                    self.postprocess_tile(specs, tls, down, right, remnant_down, remnant_right)?;
318                }
319            }
320            Ok(())
321        }
322    }
323
324    #[inline(always)]
325    unsafe fn for_valid_tile(
326        &self,
327        ker: &impl MatMatMulKer<Acc = TI>,
328        specs: &[FusedSpec],
329        tls: &mut TLSScratch,
330        down: usize,
331        right: usize,
332    ) -> TractResult<()> {
333        unsafe {
334            use FusedKerSpec as FKS;
335            use FusedSpec as FS;
336            let ScratchSpaceImpl { ker_specs, loc_dependent, .. } = self;
337            debug_assert!(specs.len() + 2 == ker_specs.len());
338            for LocDependent { spec, ker_spec, loc, buffer_a, buffer_b } in loc_dependent {
339                let spec = specs.get_unchecked(*spec);
340                let it = match spec {
341                    FS::BinPerRow(v, op) => {
342                        let v = v.as_ptr_unchecked::<TI>().add(down * ker.mr());
343                        match op {
344                            BinOp::Min => FKS::PerRowMin(v),
345                            BinOp::Max => FKS::PerRowMax(v),
346                            BinOp::Add => FKS::PerRowAdd(v),
347                            BinOp::Mul => FKS::PerRowMul(v),
348                            BinOp::Sub => FKS::PerRowSub(v),
349                            BinOp::SubF => FKS::PerRowSubF(v),
350                        }
351                    }
352                    FS::BinPerCol(v, op) => {
353                        let v = v.as_ptr_unchecked::<TI>().add(right * ker.nr());
354                        match op {
355                            BinOp::Min => FKS::PerColMin(v),
356                            BinOp::Max => FKS::PerColMax(v),
357                            BinOp::Add => FKS::PerColAdd(v),
358                            BinOp::Mul => FKS::PerColMul(v),
359                            BinOp::Sub => FKS::PerColSub(v),
360                            BinOp::SubF => FKS::PerColSubF(v),
361                        }
362                    }
363                    FS::AddRowColProducts(rows, cols) => {
364                        let row_ptr = rows.as_ptr_unchecked::<TI>().add(down * ker.mr());
365                        let col_ptr = cols.as_ptr_unchecked::<TI>().add(right * ker.nr());
366                        FKS::AddRowColProducts(row_ptr, col_ptr)
367                    }
368                    FS::AddUnicast(store) => FKS::AddUnicast(store.tile_c(down, right)),
369                    FS::Store(c_store) => FKS::Store(c_store.tile_c(down, right)),
370                    FS::AddMatMul { a, b, packing } => {
371                        let scratch = (tls.blob.as_mut_ptr().add(*loc) as *mut AddMatMulTemp)
372                            .as_mut()
373                            .unwrap();
374                        if scratch.panel_a_id != down {
375                            scratch.ptr_a = a.panel_bytes(
376                                down,
377                                buffer_a.map(|o| tls.blob.as_mut_ptr().add(o)),
378                            )?;
379                            scratch.panel_a_id = down;
380                        }
381                        if scratch.panel_b_id != right {
382                            scratch.ptr_b = b.panel_bytes(
383                                right,
384                                buffer_b.map(|o| tls.blob.as_mut_ptr().add(o)),
385                            )?;
386                            scratch.panel_b_id = right;
387                        }
388                        FKS::AddMatMul {
389                            k: b.k(),
390                            pa: scratch.ptr_a,
391                            pb: scratch.ptr_b,
392                            packing: *packing,
393                        }
394                    }
395                    _ => std::hint::unreachable_unchecked(),
396                };
397                *tls.ker_specs().get_unchecked_mut(*ker_spec) = it;
398            }
399            Ok(())
400        }
401    }
402
403    #[inline(never)]
404    #[allow(clippy::too_many_arguments)]
405    unsafe fn for_border_tile(
406        &self,
407        ker: &impl MatMatMulKer<Acc = TI>,
408        specs: &[FusedSpec],
409        tls: &mut TLSScratch,
410        down: usize,
411        right: usize,
412        m_remnant: usize,
413        n_remnant: usize,
414    ) -> TractResult<bool> {
415        unsafe {
416            use FusedKerSpec as FKS;
417            use FusedSpec as FS;
418            // sme_mmm_f32_32x32 reads a live-M count from packing bits 8..15
419            // and, on the horizontal store, writes that many rows. The count
420            // is set only together with a direct store, so the scratch copy
421            // is skipped for the same tile. Every other kernel and every
422            // short-N or non-contiguous store stays on the scratch path.
423            let direct = partial_m_direct(ker, specs, m_remnant, n_remnant);
424            for LocDependent { spec, ker_spec: uspec, loc, buffer_a, buffer_b } in
425                &self.loc_dependent
426            {
427                let loc = tls.blob.as_mut_ptr().add(*loc);
428                let spec = specs.get_unchecked(*spec);
429                let it = match spec {
430                    FS::BinPerRow(v, op) => {
431                        let buf = std::slice::from_raw_parts_mut(loc as *mut TI, ker.mr());
432                        let ptr = if m_remnant < ker.mr() {
433                            if m_remnant > 0 {
434                                buf.get_unchecked_mut(..m_remnant).copy_from_slice(
435                                    v.as_slice_unchecked()
436                                        .get_unchecked(down * ker.mr()..)
437                                        .get_unchecked(..m_remnant),
438                                );
439                            }
440                            // The kernel computes on the tail lanes before their
441                            // results are discarded; garbage there decodes to
442                            // denormals and stalls the fp pipeline. Zero them.
443                            buf.get_unchecked_mut(m_remnant..)
444                                .iter_mut()
445                                .for_each(|x| *x = TI::zero());
446                            buf.as_ptr()
447                        } else {
448                            v.as_ptr_unchecked::<TI>().add(down * ker.mr())
449                        };
450                        match op {
451                            BinOp::Min => FKS::PerRowMin(ptr),
452                            BinOp::Max => FKS::PerRowMax(ptr),
453                            BinOp::Add => FKS::PerRowAdd(ptr),
454                            BinOp::Mul => FKS::PerRowMul(ptr),
455                            BinOp::Sub => FKS::PerRowSub(ptr),
456                            BinOp::SubF => FKS::PerRowSubF(ptr),
457                        }
458                    }
459                    FS::BinPerCol(v, op) => {
460                        let buf = std::slice::from_raw_parts_mut(loc as *mut TI, ker.nr());
461                        let ptr = if n_remnant < ker.nr() {
462                            if n_remnant > 0 {
463                                buf.get_unchecked_mut(..n_remnant).copy_from_slice(
464                                    v.as_slice_unchecked()
465                                        .get_unchecked(right * ker.nr()..)
466                                        .get_unchecked(..n_remnant),
467                                );
468                            }
469                            buf.get_unchecked_mut(n_remnant..)
470                                .iter_mut()
471                                .for_each(|x| *x = TI::zero());
472                            buf.as_ptr()
473                        } else {
474                            v.as_ptr_unchecked::<TI>().add(right * ker.nr())
475                        };
476                        match op {
477                            BinOp::Min => FKS::PerColMin(ptr),
478                            BinOp::Max => FKS::PerColMax(ptr),
479                            BinOp::Add => FKS::PerColAdd(ptr),
480                            BinOp::Mul => FKS::PerColMul(ptr),
481                            BinOp::Sub => FKS::PerColSub(ptr),
482                            BinOp::SubF => FKS::PerColSubF(ptr),
483                        }
484                    }
485                    FS::AddRowColProducts(rows, cols) => {
486                        let r = std::slice::from_raw_parts_mut(loc as *mut TI, ker.mr());
487                        let row_ptr = if m_remnant < ker.mr() {
488                            r.get_unchecked_mut(..m_remnant).copy_from_slice(
489                                rows.as_slice_unchecked()
490                                    .get_unchecked(down * ker.mr()..)
491                                    .get_unchecked(..m_remnant),
492                            );
493                            r.get_unchecked_mut(m_remnant..)
494                                .iter_mut()
495                                .for_each(|x| *x = TI::zero());
496                            r.as_ptr()
497                        } else {
498                            rows.as_ptr_unchecked::<TI>().add(down * ker.mr())
499                        };
500                        let c = std::slice::from_raw_parts_mut(
501                            (loc as *mut TI).add(ker.mr()),
502                            ker.nr(),
503                        );
504                        let col_ptr = if n_remnant < ker.nr() {
505                            c.get_unchecked_mut(..n_remnant).copy_from_slice(
506                                cols.as_slice_unchecked()
507                                    .get_unchecked(right * ker.nr()..)
508                                    .get_unchecked(..n_remnant),
509                            );
510                            c.get_unchecked_mut(n_remnant..)
511                                .iter_mut()
512                                .for_each(|x| *x = TI::zero());
513                            c.as_ptr()
514                        } else {
515                            cols.as_ptr_unchecked::<TI>().add(right * ker.nr())
516                        };
517                        FKS::AddRowColProducts(row_ptr, col_ptr)
518                    }
519                    FS::AddUnicast(store) => {
520                        let row_byte_stride = store.row_byte_stride;
521                        let col_byte_stride = store.col_byte_stride;
522                        let tile_offset = row_byte_stride * down as isize * ker.mr() as isize
523                            + col_byte_stride * right as isize * ker.nr() as isize;
524                        let tile_ptr = store.ptr.offset(tile_offset);
525                        let tmp_d_tile =
526                            std::slice::from_raw_parts_mut(loc as *mut TI, ker.mr() * ker.nr());
527                        tmp_d_tile.iter_mut().for_each(|t| *t = TI::zero());
528                        for r in 0..m_remnant as isize {
529                            for c in 0..n_remnant as isize {
530                                let inner_offset = c * col_byte_stride + r * row_byte_stride;
531                                if inner_offset + tile_offset
532                                    < (store.item_size * store.item_count) as isize
533                                {
534                                    *tmp_d_tile
535                                        .get_unchecked_mut(r as usize + c as usize * ker.mr()) =
536                                        *(tile_ptr.offset(inner_offset) as *const TI);
537                                }
538                            }
539                        }
540                        FKS::AddUnicast(OutputStoreKer {
541                            ptr: tmp_d_tile.as_ptr() as _,
542                            row_byte_stride: std::mem::size_of::<TI>() as isize,
543                            col_byte_stride: (std::mem::size_of::<TI>() * ker.mr()) as isize,
544                            item_size: std::mem::size_of::<TI>(),
545                        })
546                    }
547                    FS::Store(c_store) => {
548                        if direct {
549                            FKS::Store(c_store.tile_c(down, right))
550                        } else {
551                            let row_major = ker.stores_row_major_tile()
552                                && c_store.col_byte_stride == c_store.item_size as isize;
553                            let (row_byte_stride, col_byte_stride) = if row_major {
554                                // Pad the row stride to 128 bytes so it stays aligned
555                                // for the kernel's bulk-store path.
556                                let row = Integer::next_multiple_of(
557                                    &(c_store.item_size * ker.nr()),
558                                    &128,
559                                );
560                                (row as isize, c_store.item_size as isize)
561                            } else {
562                                (
563                                    c_store.item_size as isize,
564                                    (c_store.item_size * ker.mr()) as isize,
565                                )
566                            };
567                            let tmpc = OutputStoreKer {
568                                ptr: loc as _,
569                                item_size: c_store.item_size,
570                                row_byte_stride,
571                                col_byte_stride,
572                            };
573                            FKS::Store(tmpc)
574                        }
575                    }
576                    FS::AddMatMul { a, b, packing } => {
577                        let scratch = (loc as *mut AddMatMulTemp).as_mut().unwrap();
578                        if scratch.panel_a_id != down {
579                            scratch.ptr_a = a.panel_bytes(
580                                down,
581                                buffer_a.map(|o| tls.blob.as_mut_ptr().add(o)),
582                            )?;
583                            scratch.panel_a_id = down;
584                        }
585                        if scratch.panel_b_id != right {
586                            scratch.ptr_b = b.panel_bytes(
587                                right,
588                                buffer_b.map(|o| tls.blob.as_mut_ptr().add(o)),
589                            )?;
590                            scratch.panel_b_id = right;
591                        }
592                        let packing = if direct { *packing | (m_remnant << 8) } else { *packing };
593                        FKS::AddMatMul { k: b.k(), pa: scratch.ptr_a, pb: scratch.ptr_b, packing }
594                    }
595                    _ => std::hint::unreachable_unchecked(),
596                };
597                *tls.ker_specs().get_unchecked_mut(*uspec) = it;
598            }
599            Ok(direct)
600        }
601    }
602
603    #[inline]
604    pub fn uspecs(&self) -> &[FusedKerSpec<TI>] {
605        &self.ker_specs
606    }
607
608    unsafe fn postprocess_tile(
609        &self,
610        specs: &[FusedSpec],
611        tls: &mut TLSScratch,
612        down: usize,
613        right: usize,
614        m_remnant: usize,
615        n_remnant: usize,
616    ) -> TractResult<()>
617    where
618        TI: LADatum,
619    {
620        unsafe {
621            for LocDependent { spec, ker_spec: uspec, .. } in self.loc_dependent.iter() {
622                let spec = specs.get_unchecked(*spec);
623                let ker_spec = tls.ker_specs::<TI>().get_unchecked(*uspec);
624                if let (FusedSpec::Store(c_store), FusedKerSpec::Store(tmp)) = (spec, ker_spec) {
625                    c_store.set_from_tile(down, right, m_remnant, n_remnant, tmp)
626                }
627            }
628            Ok(())
629        }
630    }
631}