Skip to main content

strided_einsum2/
contiguous.rs

1//! GEMM-ready operand types and preparation functions for contiguous data.
2//!
3//! These types encapsulate the logic for preparing strided operands for GEMM:
4//! checking fusability, copying to col-major buffers when needed, and managing
5//! the writeback for borrowed output operands.
6
7use crate::ScalarBase;
8use std::any::{Any, TypeId};
9use std::cell::RefCell;
10use std::collections::HashMap;
11use strided_view::{StridedArray, StridedView, StridedViewMut};
12
13/// GEMM-ready input operand with contiguous data.
14pub struct ContiguousOperand<T: Copy + 'static> {
15    ptr: *const T,
16    row_stride: isize,
17    col_stride: isize,
18    batch_strides: Vec<isize>,
19    conj: bool,
20    /// Owns the buffer if a copy was made or input was consumed.
21    pub(crate) _buf: Option<StridedArray<T>>,
22    buf_is_pooled: bool,
23}
24
25/// GEMM-ready output operand with contiguous data.
26pub struct ContiguousOperandMut<T: Copy + 'static> {
27    ptr: *mut T,
28    row_stride: isize,
29    col_stride: isize,
30    batch_strides: Vec<isize>,
31    /// Whether the caller must copy the buffer back to the original destination
32    /// after GEMM completes (true only for borrowed non-contiguous C).
33    needs_writeback: bool,
34    /// Owns the buffer if a copy was made.
35    pub(crate) _buf: Option<StridedArray<T>>,
36    buf_is_pooled: bool,
37}
38
39thread_local! {
40    static BUFFER_POOL: RefCell<HashMap<TypeId, Box<dyn Any>>> = RefCell::new(HashMap::new());
41}
42
43const MAX_POOL_PER_TYPE: usize = 16;
44const MAX_POOLED_BYTES: usize = 64 * 1024 * 1024;
45
46fn take_pooled_vec_uninit<T: Copy + 'static>(len: usize) -> Vec<T> {
47    BUFFER_POOL.with(|pool| {
48        let mut pool = pool.borrow_mut();
49        let entry = pool
50            .entry(TypeId::of::<T>())
51            .or_insert_with(|| Box::new(Vec::<Vec<T>>::new()));
52        let vecs = entry
53            .downcast_mut::<Vec<Vec<T>>>()
54            .expect("buffer pool type mismatch");
55
56        let mut best_idx = None;
57        let mut best_cap = usize::MAX;
58        for (idx, v) in vecs.iter().enumerate() {
59            let cap = v.capacity();
60            if cap >= len && cap < best_cap {
61                best_idx = Some(idx);
62                best_cap = cap;
63            }
64        }
65
66        let mut data = best_idx
67            .map(|idx| vecs.swap_remove(idx))
68            .unwrap_or_else(|| Vec::with_capacity(len));
69        if data.capacity() < len {
70            data.reserve(len - data.capacity());
71        }
72        unsafe { data.set_len(len) };
73        data
74    })
75}
76
77fn return_pooled_vec<T: Copy + 'static>(mut data: Vec<T>) {
78    let bytes = data.capacity().saturating_mul(std::mem::size_of::<T>());
79    if bytes == 0 || bytes > MAX_POOLED_BYTES {
80        return;
81    }
82    data.clear();
83    BUFFER_POOL.with(|pool| {
84        let mut pool = pool.borrow_mut();
85        let entry = pool
86            .entry(TypeId::of::<T>())
87            .or_insert_with(|| Box::new(Vec::<Vec<T>>::new()));
88        let vecs = entry
89            .downcast_mut::<Vec<Vec<T>>>()
90            .expect("buffer pool type mismatch");
91        if vecs.len() >= MAX_POOL_PER_TYPE {
92            if let Some((min_idx, min_cap)) = vecs
93                .iter()
94                .enumerate()
95                .map(|(i, v)| (i, v.capacity()))
96                .min_by_key(|(_, cap)| *cap)
97            {
98                if min_cap < data.capacity() {
99                    vecs.swap_remove(min_idx);
100                    vecs.push(data);
101                }
102            }
103        } else {
104            vecs.push(data);
105        }
106    });
107}
108
109fn alloc_col_major_uninit_with_pool<T: Copy + 'static>(dims: &[usize]) -> (StridedArray<T>, bool) {
110    let total: usize = dims.iter().product::<usize>().max(1);
111    let bytes = total.saturating_mul(std::mem::size_of::<T>());
112    if bytes == 0 || bytes > MAX_POOLED_BYTES {
113        return (alloc_col_major_uninit(dims), false);
114    }
115    let data = take_pooled_vec_uninit::<T>(total);
116    let arr = unsafe { StridedArray::col_major_from_buffer_uninit(data, dims) };
117    (arr, true)
118}
119
120/// Allocate a col-major buffer, optionally reusing from the thread-local pool.
121fn alloc_maybe_pooled<T: Copy + 'static>(
122    dims: &[usize],
123    use_pool: bool,
124) -> (StridedArray<T>, bool) {
125    if use_pool {
126        alloc_col_major_uninit_with_pool(dims)
127    } else {
128        (alloc_col_major_uninit(dims), false)
129    }
130}
131
132#[cfg(test)]
133fn pooled_count_for_type<T: 'static>() -> usize {
134    BUFFER_POOL.with(|pool| {
135        let mut pool = pool.borrow_mut();
136        let Some(entry) = pool.get_mut(&TypeId::of::<T>()) else {
137            return 0;
138        };
139        entry
140            .downcast_mut::<Vec<Vec<T>>>()
141            .map_or(0, |vecs| vecs.len())
142    })
143}
144
145impl<T: Copy + 'static> ContiguousOperand<T> {
146    /// Raw const pointer to the operand data at the base offset.
147    #[inline]
148    pub fn ptr(&self) -> *const T {
149        self.ptr
150    }
151
152    /// Row (lo-group) stride for the fused 2D matrix.
153    #[inline]
154    pub fn row_stride(&self) -> isize {
155        self.row_stride
156    }
157
158    /// Column (sum/ro-group) stride for the fused 2D matrix.
159    #[inline]
160    pub fn col_stride(&self) -> isize {
161        self.col_stride
162    }
163
164    /// Batch dimension strides.
165    #[inline]
166    pub fn batch_strides(&self) -> &[isize] {
167        &self.batch_strides
168    }
169
170    /// Whether this operand requires conjugation.
171    #[inline]
172    pub fn conj(&self) -> bool {
173        self.conj
174    }
175
176    /// Returns `true` if this operand owns a buffer (copy was made or ownership transferred).
177    #[cfg(test)]
178    #[inline]
179    pub(crate) fn has_buf(&self) -> bool {
180        self._buf.is_some()
181    }
182}
183
184impl<T: Copy + 'static> ContiguousOperandMut<T> {
185    /// Raw mutable pointer to the operand data at the base offset.
186    #[inline]
187    pub fn ptr(&self) -> *mut T {
188        self.ptr
189    }
190
191    /// Row (lo-group) stride for the fused 2D matrix.
192    #[inline]
193    pub fn row_stride(&self) -> isize {
194        self.row_stride
195    }
196
197    /// Column (ro-group) stride for the fused 2D matrix.
198    #[inline]
199    pub fn col_stride(&self) -> isize {
200        self.col_stride
201    }
202
203    /// Batch dimension strides.
204    #[inline]
205    pub fn batch_strides(&self) -> &[isize] {
206        &self.batch_strides
207    }
208
209    /// Returns `true` if this operand owns a buffer (copy was made).
210    #[cfg(test)]
211    #[inline]
212    pub(crate) fn has_buf(&self) -> bool {
213        self._buf.is_some()
214    }
215
216    /// Returns `true` if the caller must copy the buffer back to the original
217    /// destination after GEMM completes.
218    #[cfg(test)]
219    #[inline]
220    pub(crate) fn needs_writeback(&self) -> bool {
221        self.needs_writeback
222    }
223}
224
225impl<T: Copy + Send + Sync> ContiguousOperandMut<T> {
226    /// After GEMM: copy the internal buffer back to `dest` if needed.
227    ///
228    /// This is a no-op when the GEMM wrote directly to the destination
229    /// (contiguous case or owned output).
230    pub fn finalize_into(self, dest: &mut StridedViewMut<T>) -> crate::Result<()> {
231        if self.needs_writeback {
232            if let Some(ref buf) = self._buf {
233                strided_perm::copy_into(dest, &buf.view())?;
234            }
235        }
236        Ok(())
237    }
238}
239
240impl<T: Copy + 'static> Drop for ContiguousOperand<T> {
241    fn drop(&mut self) {
242        if self.buf_is_pooled {
243            if let Some(arr) = self._buf.take() {
244                return_pooled_vec(arr.into_data());
245            }
246        }
247    }
248}
249
250impl<T: Copy + 'static> Drop for ContiguousOperandMut<T> {
251    fn drop(&mut self) {
252        if self.buf_is_pooled {
253            if let Some(arr) = self._buf.take() {
254                return_pooled_vec(arr.into_data());
255            }
256        }
257    }
258}
259
260/// Result of checking whether dimension groups are contiguous enough for GEMM.
261struct ContiguityCheck {
262    fused_g1: Option<(usize, isize)>,
263    fused_g2: Option<(usize, isize)>,
264    needs_copy: bool,
265}
266
267/// Try to fuse a GEMM dimension group while preserving canonical col-major
268/// logical order. GEMM sees each group as a single 1D index, so independently
269/// fusing row-major and col-major groups would pair different logical indices.
270fn try_fuse_col_major_group(dims: &[usize], strides: &[isize]) -> Option<(usize, isize)> {
271    if dims.len() != strides.len() {
272        return None;
273    }
274    let total = dims
275        .iter()
276        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))?;
277    if dims.is_empty() {
278        return Some((1, 0));
279    }
280
281    let mut base_stride = None;
282    let mut expected_stride = None;
283    for (&dim, &stride) in dims.iter().zip(strides.iter()) {
284        if dim <= 1 {
285            continue;
286        }
287        if stride == 0 {
288            return None;
289        }
290        if let Some(expected) = expected_stride {
291            if stride != expected {
292                return None;
293            }
294        } else {
295            base_stride = Some(stride);
296        }
297        let dim = isize::try_from(dim).ok()?;
298        expected_stride = Some(stride.checked_mul(dim)?);
299    }
300
301    let stride = base_stride.unwrap_or_else(|| {
302        strides
303            .iter()
304            .copied()
305            .min_by_key(|stride| stride.unsigned_abs())
306            .unwrap_or(0)
307    });
308    Some((total, stride))
309}
310
311/// Check if two dimension groups are fusable (contiguous) for GEMM.
312///
313/// The fused logical dimension order must match the canonical axis order used
314/// by the plan. When `requires_unit_stride` is true (e.g., CBLAS backend), also
315/// checks that at least one of the fused strides is 0 or 1.
316fn check_contiguity(
317    group1_dims: &[usize],
318    group1_strides: &[isize],
319    group2_dims: &[usize],
320    group2_strides: &[isize],
321    requires_unit_stride: bool,
322) -> ContiguityCheck {
323    let fused_g1 = try_fuse_col_major_group(group1_dims, group1_strides);
324    let fused_g2 = try_fuse_col_major_group(group2_dims, group2_strides);
325
326    let mut needs_copy = fused_g1.is_none() || fused_g2.is_none();
327
328    if requires_unit_stride && !needs_copy {
329        let (_, rs) = fused_g1.unwrap();
330        let (_, cs) = fused_g2.unwrap();
331        if rs != 0 && rs != 1 && cs != 0 && cs != 1 {
332            needs_copy = true;
333        }
334    }
335
336    ContiguityCheck {
337        fused_g1,
338        fused_g2,
339        needs_copy,
340    }
341}
342
343/// Compute col-major layout parameters from a freshly-allocated col-major buffer.
344///
345/// Returns `(row_stride, col_stride, batch_strides)`.
346fn col_major_layout(
347    buf: &StridedArray<impl Copy>,
348    n_group1: usize,
349    n_inner: usize,
350) -> (isize, isize, Vec<isize>) {
351    let m: usize = buf.dims()[..n_group1].iter().product::<usize>().max(1);
352    let row_stride = if m == 0 { 0 } else { 1isize };
353    let col_stride = m as isize;
354    let batch_strides = buf.strides()[n_inner..].to_vec();
355    (row_stride, col_stride, batch_strides)
356}
357
358/// Allocate a column-major StridedArray with uninitialized data.
359///
360/// With batch-last canonical order `[inner..., batch...]`, pure column-major
361/// naturally gives batch dims the largest strides — each batch slice is a
362/// contiguous column-major matrix.
363pub(crate) fn alloc_col_major_uninit<T: Copy>(dims: &[usize]) -> StridedArray<T> {
364    let total: usize = dims.iter().product::<usize>().max(1);
365    // SAFETY: `T: Copy` guarantees no drop glue, so leaving elements
366    // uninitialised is safe. Every call-site writes all elements before
367    // reading: A and B via `copy_into`, C via `copy_into` (beta != 0)
368    // or GEMM with replace semantics (beta == 0).
369    let mut data = Vec::with_capacity(total);
370    unsafe { data.set_len(total) };
371
372    // Pure column-major: stride 1 for first dim, each subsequent dim
373    // has stride = previous stride * previous dim size.
374    let mut strides = vec![0isize; dims.len()];
375    if !dims.is_empty() {
376        strides[0] = 1;
377        for i in 1..dims.len() {
378            strides[i] = strides[i - 1] * dims[i - 1] as isize;
379        }
380    }
381
382    let arr = StridedArray::from_parts(data, dims, &strides, 0).expect("col-major allocation");
383    arr
384}
385
386/// Prepare a borrowed input view for GEMM.
387///
388/// Expects batch-last canonical order: `[group1..., group2..., batch...]`.
389/// Checks if the two inner dimension groups are fusable.
390/// If not, copies to a contiguous col-major buffer.
391///
392/// - `requires_unit_stride`: backend needs at least one unit stride (e.g. CBLAS).
393/// - `use_pool`: reuse thread-local buffers to avoid repeated allocation.
394/// - `materialize_conj_fn`: when `Some(f)` and `conj == true`, applies `f` to each
395///   element during copy (for backends that cannot pass conj flags to GEMM).
396pub fn prepare_input_view<T: ScalarBase + 'static>(
397    view: &StridedView<T>,
398    n_group1: usize,
399    n_group2: usize,
400    conj: bool,
401    requires_unit_stride: bool,
402    use_pool: bool,
403    materialize_conj_fn: Option<fn(T) -> T>,
404) -> crate::Result<ContiguousOperand<T>> {
405    let dims = view.dims();
406    let strides = view.strides();
407    let n_inner = n_group1 + n_group2;
408
409    // For backends that cannot pass conjugation flags to GEMM (e.g., CBLAS),
410    // materialize conj into the data before the GEMM call.
411    if let Some(conj_fn) = materialize_conj_fn {
412        if conj {
413            let (mut buf, buf_is_pooled) = alloc_maybe_pooled(dims, use_pool);
414            strided_kernel::map_into(&mut buf.view_mut(), view, conj_fn)?;
415            let ptr = buf.view().ptr();
416            let (row_stride, col_stride, batch_strides) = col_major_layout(&buf, n_group1, n_inner);
417            return Ok(ContiguousOperand {
418                ptr,
419                row_stride,
420                col_stride,
421                batch_strides,
422                conj: false,
423                _buf: Some(buf),
424                buf_is_pooled,
425            });
426        }
427    }
428
429    let check = check_contiguity(
430        &dims[..n_group1],
431        &strides[..n_group1],
432        &dims[n_group1..n_inner],
433        &strides[n_group1..n_inner],
434        requires_unit_stride,
435    );
436
437    if check.needs_copy {
438        let (mut buf, buf_is_pooled) = alloc_maybe_pooled(dims, use_pool);
439        strided_kernel::copy_into_col_major(&mut buf.view_mut(), view)?;
440        let ptr = buf.view().ptr();
441        let (row_stride, col_stride, batch_strides) = col_major_layout(&buf, n_group1, n_inner);
442        Ok(ContiguousOperand {
443            ptr,
444            row_stride,
445            col_stride,
446            batch_strides,
447            conj,
448            _buf: Some(buf),
449            buf_is_pooled,
450        })
451    } else {
452        let (_, rs) = check.fused_g1.unwrap();
453        let (_, cs) = check.fused_g2.unwrap();
454        Ok(ContiguousOperand {
455            ptr: view.ptr(),
456            row_stride: rs,
457            col_stride: cs,
458            batch_strides: strides[n_inner..].to_vec(),
459            conj,
460            _buf: None,
461            buf_is_pooled: false,
462        })
463    }
464}
465
466/// Prepare an owned input array for GEMM.
467///
468/// Expects batch-last canonical order: `[group1..., group2..., batch...]`.
469/// If already contiguous after dimension grouping, transfers ownership without copying.
470/// Otherwise, copies to a new col-major buffer.
471///
472/// Parameters are the same as [`prepare_input_view`].
473pub fn prepare_input_owned<T: ScalarBase + 'static>(
474    arr: StridedArray<T>,
475    n_group1: usize,
476    n_group2: usize,
477    conj: bool,
478    requires_unit_stride: bool,
479    use_pool: bool,
480    materialize_conj_fn: Option<fn(T) -> T>,
481) -> crate::Result<ContiguousOperand<T>> {
482    let dims = arr.dims().to_vec();
483    let strides = arr.strides().to_vec();
484    let n_inner = n_group1 + n_group2;
485
486    // For backends that cannot pass conjugation flags to GEMM,
487    // materialize conj into the data before the GEMM call.
488    if let Some(conj_fn) = materialize_conj_fn {
489        if conj {
490            let (mut buf, buf_is_pooled) = alloc_maybe_pooled(&dims, use_pool);
491            strided_kernel::map_into(&mut buf.view_mut(), &arr.view(), conj_fn)?;
492            let ptr = buf.view().ptr();
493            let (row_stride, col_stride, batch_strides) = col_major_layout(&buf, n_group1, n_inner);
494            return Ok(ContiguousOperand {
495                ptr,
496                row_stride,
497                col_stride,
498                batch_strides,
499                conj: false,
500                _buf: Some(buf),
501                buf_is_pooled,
502            });
503        }
504    }
505
506    let check = check_contiguity(
507        &dims[..n_group1],
508        &strides[..n_group1],
509        &dims[n_group1..n_inner],
510        &strides[n_group1..n_inner],
511        requires_unit_stride,
512    );
513
514    if check.needs_copy {
515        let (mut buf, buf_is_pooled) = alloc_maybe_pooled(&dims, use_pool);
516        strided_kernel::copy_into_col_major(&mut buf.view_mut(), &arr.view())?;
517        let ptr = buf.view().ptr();
518        let (row_stride, col_stride, batch_strides) = col_major_layout(&buf, n_group1, n_inner);
519        Ok(ContiguousOperand {
520            ptr,
521            row_stride,
522            col_stride,
523            batch_strides,
524            conj,
525            _buf: Some(buf),
526            buf_is_pooled,
527        })
528    } else {
529        let (_, rs) = check.fused_g1.unwrap();
530        let (_, cs) = check.fused_g2.unwrap();
531        let ptr = arr.view().ptr();
532        Ok(ContiguousOperand {
533            ptr,
534            row_stride: rs,
535            col_stride: cs,
536            batch_strides: strides[n_inner..].to_vec(),
537            conj,
538            _buf: Some(arr),
539            buf_is_pooled: false,
540        })
541    }
542}
543
544/// Prepare a borrowed mutable output view for GEMM.
545///
546/// Expects batch-last canonical order: `[group1..., group2..., batch...]`.
547/// Checks if the two inner dimension groups (lo, ro) are fusable.
548/// If not, allocates a col-major buffer and copies the existing data into it
549/// when `beta` is non-zero (so the GEMM accumulation is correct).
550///
551/// After GEMM, call [`ContiguousOperandMut::finalize_into`] with the original
552/// view to copy results back if needed.
553///
554/// # Safety contract
555///
556/// When inner dims are fusable (no copy needed), the returned `ContiguousOperandMut`
557/// holds a raw pointer into `view`'s data. The caller must ensure `view` outlives
558/// the returned operand and that no aliasing mutable references exist during GEMM.
559pub fn prepare_output_view<T: ScalarBase + 'static>(
560    view: &mut StridedViewMut<T>,
561    n_group1: usize,
562    n_group2: usize,
563    beta: T,
564    requires_unit_stride: bool,
565    use_pool: bool,
566) -> crate::Result<ContiguousOperandMut<T>> {
567    let dims = view.dims().to_vec();
568    let strides = view.strides().to_vec();
569    let n_inner = n_group1 + n_group2;
570
571    let check = check_contiguity(
572        &dims[..n_group1],
573        &strides[..n_group1],
574        &dims[n_group1..n_inner],
575        &strides[n_group1..n_inner],
576        requires_unit_stride,
577    );
578
579    if check.needs_copy {
580        let (mut buf, buf_is_pooled) = alloc_maybe_pooled(&dims, use_pool);
581        if beta != T::zero() {
582            strided_kernel::copy_into_col_major(&mut buf.view_mut(), &view.as_view())?;
583        }
584        let ptr = buf.view_mut().as_mut_ptr();
585        let (row_stride, col_stride, batch_strides) = col_major_layout(&buf, n_group1, n_inner);
586        Ok(ContiguousOperandMut {
587            ptr,
588            row_stride,
589            col_stride,
590            batch_strides,
591            needs_writeback: true,
592            _buf: Some(buf),
593            buf_is_pooled,
594        })
595    } else {
596        let (_, rs) = check.fused_g1.unwrap();
597        let (_, cs) = check.fused_g2.unwrap();
598        Ok(ContiguousOperandMut {
599            ptr: view.as_mut_ptr(),
600            row_stride: rs,
601            col_stride: cs,
602            batch_strides: strides[n_inner..].to_vec(),
603            needs_writeback: false,
604            _buf: None,
605            buf_is_pooled: false,
606        })
607    }
608}
609
610#[cfg(test)]
611mod tests_generic_backend {
612    use super::*;
613    use crate::backend::{Backend, NaiveBackend};
614
615    #[test]
616    fn test_input_for_backend_contiguous() {
617        let a = StridedArray::<f64>::col_major(&[2, 3]);
618        let view = a.view();
619        let op = prepare_input_view(
620            &view,
621            1,
622            1,
623            false,
624            <NaiveBackend as Backend<f64>>::REQUIRES_UNIT_STRIDE,
625            false,
626            None,
627        )
628        .unwrap();
629        assert!(op._buf.is_none());
630        assert_eq!(op.row_stride(), 1);
631        assert_eq!(op.col_stride(), 2);
632        assert!(!op.conj());
633    }
634
635    #[test]
636    fn test_input_for_backend_non_contiguous() {
637        let data = vec![0.0f64; 100];
638        let a = StridedArray::<f64>::from_parts(data, &[2, 3, 4], &[20, 4, 1], 0).unwrap();
639        let view = a.view();
640        let op = prepare_input_view(
641            &view,
642            2,
643            1,
644            false,
645            <NaiveBackend as Backend<f64>>::REQUIRES_UNIT_STRIDE,
646            false,
647            None,
648        )
649        .unwrap();
650        assert!(op._buf.is_some());
651        assert_eq!(op.row_stride(), 1);
652        assert_eq!(op.col_stride(), 6);
653    }
654
655    #[test]
656    fn test_output_for_backend_contiguous() {
657        let mut c = StridedArray::<f64>::col_major(&[2, 3]);
658        let mut view = c.view_mut();
659        let op = prepare_output_view(
660            &mut view,
661            1,
662            1,
663            0.0,
664            <NaiveBackend as Backend<f64>>::REQUIRES_UNIT_STRIDE,
665            false,
666        )
667        .unwrap();
668        assert!(!op.needs_writeback);
669        assert!(op._buf.is_none());
670        assert_eq!(op.row_stride(), 1);
671        assert_eq!(op.col_stride(), 2);
672    }
673
674    #[test]
675    fn test_output_for_backend_non_contiguous_beta_zero() {
676        let data = vec![0.0f64; 100];
677        let mut c = StridedArray::<f64>::from_parts(data, &[2, 3, 4], &[20, 4, 1], 0).unwrap();
678        let mut view = c.view_mut();
679        let op = prepare_output_view(
680            &mut view,
681            2,
682            1,
683            0.0,
684            <NaiveBackend as Backend<f64>>::REQUIRES_UNIT_STRIDE,
685            false,
686        )
687        .unwrap();
688        assert!(op.needs_writeback);
689        assert!(op._buf.is_some());
690        assert_eq!(op.row_stride(), 1);
691        assert_eq!(op.col_stride(), 6);
692    }
693
694    #[test]
695    fn test_output_for_backend_non_contiguous_beta_nonzero_and_finalize() {
696        let mut data = vec![0.0f64; 30];
697        data[0] = 10.0;
698        data[1] = 20.0;
699        data[10] = 40.0;
700        let mut c = StridedArray::<f64>::from_parts(data, &[2, 3, 1], &[10, 1, 1], 0).unwrap();
701        let mut view = c.view_mut();
702        let op = prepare_output_view(
703            &mut view,
704            2,
705            1,
706            1.0,
707            <NaiveBackend as Backend<f64>>::REQUIRES_UNIT_STRIDE,
708            false,
709        )
710        .unwrap();
711        assert!(op.needs_writeback);
712        let buf = op._buf.as_ref().unwrap();
713        assert_eq!(buf.get(&[0, 0, 0]), 10.0);
714        assert_eq!(buf.get(&[0, 1, 0]), 20.0);
715        assert_eq!(buf.get(&[1, 0, 0]), 40.0);
716        op.finalize_into(&mut view).unwrap();
717    }
718}
719
720#[cfg(test)]
721mod tests {
722    use super::*;
723    use crate::backend::{ActiveBackend, Backend};
724
725    // Helper to construct prepare_input_view params matching the active backend.
726    const UNIT_STRIDE: bool = <ActiveBackend as Backend<f64>>::REQUIRES_UNIT_STRIDE;
727
728    #[test]
729    fn test_borrowed_contiguous_no_copy() {
730        let a = StridedArray::<f64>::col_major(&[2, 3]);
731        let view = a.view();
732
733        let op = prepare_input_view(&view, 1, 1, false, UNIT_STRIDE, true, None).unwrap();
734
735        assert!(!op.has_buf());
736        assert_eq!(op.row_stride(), 1);
737        assert_eq!(op.col_stride(), 2);
738        assert!(!op.conj());
739    }
740
741    #[test]
742    fn test_borrowed_transposed_matrix_no_copy() {
743        let data = vec![0.0f64; 6];
744        let a_t = StridedArray::<f64>::from_parts(data, &[2, 3], &[3, 1], 0).unwrap();
745        let view = a_t.view();
746
747        let op = prepare_input_view(&view, 1, 1, false, UNIT_STRIDE, true, None).unwrap();
748
749        assert!(!op.has_buf());
750        assert_eq!(op.row_stride(), 3);
751        assert_eq!(op.col_stride(), 1);
752    }
753
754    #[test]
755    fn test_borrowed_batched_transposed_matrix_no_copy() {
756        let data = vec![0.0f64; 2 * 3 * 5];
757        let a_t = StridedArray::<f64>::from_parts(data, &[2, 3, 5], &[3, 1, 6], 0).unwrap();
758        let view = a_t.view();
759
760        let op = prepare_input_view(&view, 1, 1, false, UNIT_STRIDE, true, None).unwrap();
761
762        assert!(!op.has_buf());
763        assert_eq!(op.row_stride(), 3);
764        assert_eq!(op.col_stride(), 1);
765        assert_eq!(op.batch_strides(), &[6]);
766    }
767
768    #[test]
769    fn test_borrowed_non_contiguous_copies() {
770        let data = vec![0.0f64; 100];
771        let a = StridedArray::<f64>::from_parts(data, &[2, 3, 4], &[20, 4, 1], 0).unwrap();
772        let view = a.view();
773
774        let op = prepare_input_view(&view, 2, 1, false, UNIT_STRIDE, true, None).unwrap();
775
776        assert!(op.has_buf());
777        assert_eq!(op.row_stride(), 1);
778        assert_eq!(op.col_stride(), 6);
779    }
780
781    #[test]
782    fn test_owned_contiguous_no_copy() {
783        let a = StridedArray::<f64>::col_major(&[2, 3]);
784
785        let op = prepare_input_owned(a, 1, 1, false, UNIT_STRIDE, true, None).unwrap();
786
787        assert!(op.has_buf());
788        assert_eq!(op.row_stride(), 1);
789        assert_eq!(op.col_stride(), 2);
790    }
791
792    #[test]
793    fn test_owned_non_contiguous_copies() {
794        let data = vec![0.0f64; 100];
795        let a = StridedArray::<f64>::from_parts(data, &[2, 3, 4], &[20, 4, 1], 0).unwrap();
796
797        let op = prepare_input_owned(a, 2, 1, false, UNIT_STRIDE, true, None).unwrap();
798
799        assert!(op.has_buf());
800        assert_eq!(op.row_stride(), 1);
801        assert_eq!(op.col_stride(), 6);
802    }
803
804    #[test]
805    fn test_output_view_contiguous() {
806        let mut c = StridedArray::<f64>::col_major(&[2, 3]);
807        let mut view = c.view_mut();
808
809        let op = prepare_output_view(&mut view, 1, 1, 0.0, UNIT_STRIDE, true).unwrap();
810
811        assert!(!op.needs_writeback());
812        assert!(!op.has_buf());
813        assert_eq!(op.row_stride(), 1);
814        assert_eq!(op.col_stride(), 2);
815    }
816
817    #[test]
818    fn test_output_view_non_contiguous_beta_zero() {
819        let data = vec![0.0f64; 100];
820        let mut c = StridedArray::<f64>::from_parts(data, &[2, 3, 4], &[20, 4, 1], 0).unwrap();
821        let mut view = c.view_mut();
822
823        let op = prepare_output_view(&mut view, 2, 1, 0.0, UNIT_STRIDE, true).unwrap();
824
825        assert!(op.needs_writeback());
826        assert!(op.has_buf());
827        assert_eq!(op.row_stride(), 1);
828        assert_eq!(op.col_stride(), 6);
829    }
830
831    #[test]
832    fn test_output_view_non_contiguous_beta_nonzero_and_finalize() {
833        let mut data = vec![0.0f64; 30];
834        data[0] = 10.0;
835        data[1] = 20.0;
836        data[2] = 30.0;
837        data[10] = 40.0;
838        data[11] = 50.0;
839        data[12] = 60.0;
840        let mut c = StridedArray::<f64>::from_parts(data, &[2, 3, 1], &[10, 1, 1], 0).unwrap();
841
842        assert_eq!(c.get(&[0, 0, 0]), 10.0);
843        assert_eq!(c.get(&[1, 1, 0]), 50.0);
844
845        let mut view = c.view_mut();
846
847        let mut op = prepare_output_view(&mut view, 2, 1, 1.0, UNIT_STRIDE, true).unwrap();
848
849        assert!(op.needs_writeback());
850        assert!(op.has_buf());
851
852        let buf = op._buf.as_ref().unwrap();
853        assert_eq!(buf.get(&[0, 0, 0]), 10.0);
854        assert_eq!(buf.get(&[1, 1, 0]), 50.0);
855
856        {
857            let result_data = vec![100.0f64; 6];
858            let result =
859                StridedArray::<f64>::from_parts(result_data, &[2, 3, 1], &[3, 1, 1], 0).unwrap();
860            strided_kernel::copy_into(&mut op._buf.as_mut().unwrap().view_mut(), &result.view())
861                .unwrap();
862            op.ptr = op._buf.as_mut().unwrap().view_mut().as_mut_ptr();
863        }
864
865        op.finalize_into(&mut view).unwrap();
866
867        assert_eq!(c.get(&[0, 0, 0]), 100.0);
868        assert_eq!(c.get(&[0, 1, 0]), 100.0);
869        assert_eq!(c.get(&[0, 2, 0]), 100.0);
870        assert_eq!(c.get(&[1, 0, 0]), 100.0);
871        assert_eq!(c.get(&[1, 1, 0]), 100.0);
872        assert_eq!(c.get(&[1, 2, 0]), 100.0);
873    }
874
875    #[test]
876    fn test_prepare_input_view_temp_buffer_is_recycled() {
877        let before = pooled_count_for_type::<f64>();
878        let data = vec![0.0f64; 100];
879        let a = StridedArray::<f64>::from_parts(data, &[2, 3, 4], &[20, 4, 1], 0).unwrap();
880        let view = a.view();
881
882        {
883            let op = prepare_input_view(&view, 2, 1, false, UNIT_STRIDE, true, None).unwrap();
884            assert!(op.has_buf());
885        }
886
887        let after = pooled_count_for_type::<f64>();
888        assert!(after >= before.saturating_add(1));
889    }
890}