Skip to main content

tenferro_tensor/
backend.rs

1use crate::config::{
2    CompareDir, DotGeneralConfig, GatherConfig, PadConfig, ScatterConfig, SliceConfig,
3};
4use crate::types::{
5    Buffer, TensorRank, TensorView, TensorViewMut, TypedTensor, TypedTensorView, TypedTensorViewMut,
6};
7use crate::validate::validate_convert_dtype;
8use crate::{DType, RuntimeCacheControl, Tensor, TensorRead, TensorValue, TensorWrite};
9use num_complex::{Complex32, Complex64};
10
11fn read_boundary_error(op: &'static str) -> crate::Error {
12    crate::Error::backend_failure(
13        op,
14        "backend does not accept borrowed tensor views at this execution boundary",
15    )
16}
17
18fn read_tensor<'a>(op: &'static str, input: TensorRead<'a>) -> crate::Result<&'a Tensor> {
19    input.as_tensor().ok_or_else(|| read_boundary_error(op))
20}
21
22fn validate_axis_list(
23    op: &'static str,
24    role: &'static str,
25    axes: &[usize],
26    rank: usize,
27) -> crate::Result<()> {
28    let mut seen = vec![false; rank];
29    for &axis in axes {
30        if axis >= rank {
31            return Err(crate::Error::AxisOutOfBounds { op, axis, rank });
32        }
33        if seen[axis] {
34            return Err(crate::Error::DuplicateAxis { op, axis, role });
35        }
36        seen[axis] = true;
37    }
38    Ok(())
39}
40
41fn validate_role_disjoint(
42    op: &'static str,
43    first_role: &'static str,
44    first_axes: &[usize],
45    second_role: &'static str,
46    second_axes: &[usize],
47) -> crate::Result<()> {
48    for &axis in first_axes {
49        if second_axes.contains(&axis) {
50            return Err(crate::Error::AxisRoleConflict {
51                op,
52                axis,
53                first_role,
54                second_role,
55            });
56        }
57    }
58    Ok(())
59}
60
61/// Infer the output shape for a validated dot-general operation.
62#[doc(hidden)]
63pub fn dot_general_output_shape(
64    lhs_shape: &[usize],
65    rhs_shape: &[usize],
66    config: &DotGeneralConfig,
67    op: &'static str,
68) -> crate::Result<Vec<usize>> {
69    if config.lhs_contracting_dims.len() != config.rhs_contracting_dims.len() {
70        return Err(crate::Error::InvalidConfig {
71            op,
72            message: "lhs/rhs contracting dim counts differ".into(),
73        });
74    }
75    if config.lhs_batch_dims.len() != config.rhs_batch_dims.len() {
76        return Err(crate::Error::InvalidConfig {
77            op,
78            message: "lhs/rhs batch dim counts differ".into(),
79        });
80    }
81
82    let lhs_rank = lhs_shape.len();
83    let rhs_rank = rhs_shape.len();
84    validate_axis_list(
85        op,
86        "lhs_contracting",
87        &config.lhs_contracting_dims,
88        lhs_rank,
89    )?;
90    validate_axis_list(
91        op,
92        "rhs_contracting",
93        &config.rhs_contracting_dims,
94        rhs_rank,
95    )?;
96    validate_axis_list(op, "lhs_batch", &config.lhs_batch_dims, lhs_rank)?;
97    validate_axis_list(op, "rhs_batch", &config.rhs_batch_dims, rhs_rank)?;
98    validate_role_disjoint(
99        op,
100        "lhs_contracting",
101        &config.lhs_contracting_dims,
102        "lhs_batch",
103        &config.lhs_batch_dims,
104    )?;
105    validate_role_disjoint(
106        op,
107        "rhs_contracting",
108        &config.rhs_contracting_dims,
109        "rhs_batch",
110        &config.rhs_batch_dims,
111    )?;
112
113    for (&lhs_axis, &rhs_axis) in config
114        .lhs_contracting_dims
115        .iter()
116        .zip(&config.rhs_contracting_dims)
117    {
118        if lhs_shape[lhs_axis] != rhs_shape[rhs_axis] {
119            return Err(crate::Error::ShapeMismatch {
120                op,
121                lhs: lhs_shape.to_vec(),
122                rhs: rhs_shape.to_vec(),
123            });
124        }
125    }
126    for (&lhs_axis, &rhs_axis) in config.lhs_batch_dims.iter().zip(&config.rhs_batch_dims) {
127        if lhs_shape[lhs_axis] != rhs_shape[rhs_axis] {
128            return Err(crate::Error::ShapeMismatch {
129                op,
130                lhs: lhs_shape.to_vec(),
131                rhs: rhs_shape.to_vec(),
132            });
133        }
134    }
135
136    let lhs_free = (0..lhs_rank)
137        .filter(|axis| {
138            !config.lhs_contracting_dims.contains(axis) && !config.lhs_batch_dims.contains(axis)
139        })
140        .map(|axis| lhs_shape[axis]);
141    let rhs_free = (0..rhs_rank)
142        .filter(|axis| {
143            !config.rhs_contracting_dims.contains(axis) && !config.rhs_batch_dims.contains(axis)
144        })
145        .map(|axis| rhs_shape[axis]);
146    let batch = config.lhs_batch_dims.iter().map(|&axis| lhs_shape[axis]);
147
148    Ok(lhs_free.chain(rhs_free).chain(batch).collect())
149}
150
151/// Validate output dtype and shape for dot-general read-into dispatch.
152#[doc(hidden)]
153pub fn validate_dot_general_read_into(
154    lhs: &TensorRead<'_>,
155    rhs: &TensorRead<'_>,
156    config: &DotGeneralConfig,
157    out: &TensorWrite<'_>,
158    op: &'static str,
159) -> crate::Result<Vec<usize>> {
160    if lhs.dtype() != rhs.dtype() {
161        return Err(crate::Error::DTypeMismatch {
162            op,
163            lhs: lhs.dtype(),
164            rhs: rhs.dtype(),
165        });
166    }
167    if out.dtype() != lhs.dtype() {
168        return Err(crate::Error::DTypeMismatch {
169            op,
170            lhs: out.dtype(),
171            rhs: lhs.dtype(),
172        });
173    }
174    let expected = dot_general_output_shape(lhs.shape(), rhs.shape(), config, op)?;
175    if out.shape() != expected.as_slice() {
176        return Err(crate::Error::ShapeMismatch {
177            op,
178            lhs: out.shape().to_vec(),
179            rhs: expected.clone(),
180        });
181    }
182    Ok(expected)
183}
184
185/// Scalar coefficient accepted by contraction accumulation backends.
186///
187/// `ContractionScalar` is intentionally narrower than [`crate::TensorScalar`]:
188/// dot-general accumulation is only defined for floating and complex tensor
189/// dtypes.
190///
191/// # Examples
192///
193/// ```rust
194/// use tenferro_tensor::{ContractionScalar, DType};
195///
196/// let alpha = ContractionScalar::F64(2.0);
197/// assert_eq!(alpha.dtype(), DType::F64);
198/// ```
199#[derive(Clone, Copy, Debug, PartialEq)]
200pub enum ContractionScalar {
201    F32(f32),
202    F64(f64),
203    C32(Complex32),
204    C64(Complex64),
205}
206
207impl ContractionScalar {
208    /// Return this scalar's tensor dtype.
209    ///
210    /// # Examples
211    ///
212    /// ```rust
213    /// use tenferro_tensor::{ContractionScalar, DType};
214    ///
215    /// assert_eq!(ContractionScalar::F32(1.0).dtype(), DType::F32);
216    /// ```
217    pub fn dtype(self) -> DType {
218        match self {
219            Self::F32(_) => DType::F32,
220            Self::F64(_) => DType::F64,
221            Self::C32(_) => DType::C32,
222            Self::C64(_) => DType::C64,
223        }
224    }
225
226    /// Return the multiplicative identity for a supported contraction dtype.
227    ///
228    /// # Examples
229    ///
230    /// ```rust
231    /// use tenferro_tensor::{ContractionScalar, DType};
232    ///
233    /// assert_eq!(ContractionScalar::one(DType::F64).unwrap(), ContractionScalar::F64(1.0));
234    /// assert!(ContractionScalar::one(DType::I32).is_err());
235    /// ```
236    pub fn one(dtype: DType) -> crate::Result<Self> {
237        match dtype {
238            DType::F32 => Ok(Self::F32(1.0)),
239            DType::F64 => Ok(Self::F64(1.0)),
240            DType::C32 => Ok(Self::C32(Complex32::new(1.0, 0.0))),
241            DType::C64 => Ok(Self::C64(Complex64::new(1.0, 0.0))),
242            DType::I32 | DType::I64 | DType::Bool => Err(crate::Error::DTypeMismatch {
243                op: "dot_general",
244                lhs: dtype,
245                rhs: DType::F32,
246            }),
247        }
248    }
249
250    /// Return the additive identity for a supported contraction dtype.
251    ///
252    /// # Examples
253    ///
254    /// ```rust
255    /// use tenferro_tensor::{ContractionScalar, DType};
256    ///
257    /// assert_eq!(ContractionScalar::zero(DType::F64).unwrap(), ContractionScalar::F64(0.0));
258    /// ```
259    pub fn zero(dtype: DType) -> crate::Result<Self> {
260        match dtype {
261            DType::F32 => Ok(Self::F32(0.0)),
262            DType::F64 => Ok(Self::F64(0.0)),
263            DType::C32 => Ok(Self::C32(Complex32::new(0.0, 0.0))),
264            DType::C64 => Ok(Self::C64(Complex64::new(0.0, 0.0))),
265            DType::I32 | DType::I64 | DType::Bool => Err(crate::Error::DTypeMismatch {
266                op: "dot_general",
267                lhs: dtype,
268                rhs: DType::F32,
269            }),
270        }
271    }
272}
273
274/// Output-update semantics for dot-general accumulation.
275///
276/// This keeps contraction axes in [`DotGeneralConfig`] and output update
277/// semantics here, so cached and non-cached backend traits can share the same
278/// accumulation contract.
279///
280/// # Examples
281///
282/// ```rust
283/// use tenferro_tensor::{ContractionScalar, DotGeneralAccumulation, DType};
284///
285/// let accum = DotGeneralAccumulation::overwrite(DType::F64).unwrap();
286/// assert_eq!(accum.alpha, ContractionScalar::F64(1.0));
287/// assert_eq!(accum.beta, ContractionScalar::F64(0.0));
288/// ```
289#[derive(Clone, Copy, Debug, PartialEq)]
290pub struct DotGeneralAccumulation {
291    pub lhs_conj: bool,
292    pub rhs_conj: bool,
293    pub alpha: ContractionScalar,
294    pub beta: ContractionScalar,
295}
296
297/// One matrix multiply in a grouped GEMM over shared flat buffers.
298///
299/// Offsets are element offsets into the corresponding shared lhs, rhs, and
300/// output buffers. Each job computes a column-major `rows x cols` output block
301/// from a column-major `rows x contracted` lhs block and a column-major
302/// `contracted x cols` rhs block.
303#[doc(hidden)]
304#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
305pub struct GroupedGemmJob {
306    out_offset: usize,
307    lhs_offset: usize,
308    rhs_offset: usize,
309    rows: usize,
310    contracted: usize,
311    cols: usize,
312}
313
314impl GroupedGemmJob {
315    #[allow(clippy::too_many_arguments)]
316    pub fn new(
317        out_offset: usize,
318        lhs_offset: usize,
319        rhs_offset: usize,
320        rows: usize,
321        contracted: usize,
322        cols: usize,
323    ) -> Self {
324        Self {
325            out_offset,
326            lhs_offset,
327            rhs_offset,
328            rows,
329            contracted,
330            cols,
331        }
332    }
333
334    pub fn out_offset(&self) -> usize {
335        self.out_offset
336    }
337
338    pub fn lhs_offset(&self) -> usize {
339        self.lhs_offset
340    }
341
342    pub fn rhs_offset(&self) -> usize {
343        self.rhs_offset
344    }
345
346    pub fn rows(&self) -> usize {
347        self.rows
348    }
349
350    pub fn contracted(&self) -> usize {
351        self.contracted
352    }
353
354    pub fn cols(&self) -> usize {
355        self.cols
356    }
357}
358
359/// Shared scalar/update metadata for grouped GEMM execution.
360#[doc(hidden)]
361#[derive(Clone, Copy, Debug, PartialEq)]
362pub struct GroupedGemmConfig<'a> {
363    jobs: &'a [GroupedGemmJob],
364    accumulation: DotGeneralAccumulation,
365}
366
367impl<'a> GroupedGemmConfig<'a> {
368    pub fn new(jobs: &'a [GroupedGemmJob], accumulation: DotGeneralAccumulation) -> Self {
369        Self { jobs, accumulation }
370    }
371
372    pub fn jobs(&self) -> &'a [GroupedGemmJob] {
373        self.jobs
374    }
375
376    pub fn accumulation(&self) -> DotGeneralAccumulation {
377        self.accumulation
378    }
379}
380
381impl DotGeneralAccumulation {
382    /// Return overwrite semantics, `out = lhs dot rhs`, for `dtype`.
383    pub fn overwrite(dtype: DType) -> crate::Result<Self> {
384        Ok(Self {
385            lhs_conj: false,
386            rhs_conj: false,
387            alpha: ContractionScalar::one(dtype)?,
388            beta: ContractionScalar::zero(dtype)?,
389        })
390    }
391
392    /// Return additive update semantics, `out += lhs dot rhs`, for `dtype`.
393    ///
394    /// # Examples
395    ///
396    /// ```rust
397    /// use tenferro_tensor::{ContractionScalar, DType, DotGeneralAccumulation};
398    ///
399    /// let accum = DotGeneralAccumulation::add_to(DType::F64)?;
400    /// assert_eq!(accum.alpha, ContractionScalar::F64(1.0));
401    /// assert_eq!(accum.beta, ContractionScalar::F64(1.0));
402    /// # Ok::<(), tenferro_tensor::Error>(())
403    /// ```
404    pub fn add_to(dtype: DType) -> crate::Result<Self> {
405        Ok(Self {
406            lhs_conj: false,
407            rhs_conj: false,
408            alpha: ContractionScalar::one(dtype)?,
409            beta: ContractionScalar::one(dtype)?,
410        })
411    }
412
413    /// Return scaled update semantics, `out = alpha * lhs dot rhs + beta * out`.
414    ///
415    /// # Examples
416    ///
417    /// ```rust
418    /// use tenferro_tensor::{ContractionScalar, DotGeneralAccumulation};
419    ///
420    /// let accum = DotGeneralAccumulation::scaled(
421    ///     ContractionScalar::F32(0.5),
422    ///     ContractionScalar::F32(2.0),
423    /// )?;
424    /// assert_eq!(accum.alpha, ContractionScalar::F32(0.5));
425    /// # Ok::<(), tenferro_tensor::Error>(())
426    /// ```
427    pub fn scaled(alpha: ContractionScalar, beta: ContractionScalar) -> crate::Result<Self> {
428        if alpha.dtype() != beta.dtype() {
429            return Err(crate::Error::DTypeMismatch {
430                op: "dot_general",
431                lhs: alpha.dtype(),
432                rhs: beta.dtype(),
433            });
434        }
435        Ok(Self {
436            lhs_conj: false,
437            rhs_conj: false,
438            alpha,
439            beta,
440        })
441    }
442
443    fn validate_for_dtype(self, dtype: DType) -> crate::Result<()> {
444        for scalar in [self.alpha, self.beta] {
445            if scalar.dtype() != dtype {
446                return Err(crate::Error::DTypeMismatch {
447                    op: "dot_general",
448                    lhs: scalar.dtype(),
449                    rhs: dtype,
450                });
451            }
452        }
453        Ok(())
454    }
455}
456
457#[doc(hidden)]
458pub fn validate_dot_general_accumulation(
459    lhs: &TensorRead<'_>,
460    rhs: &TensorRead<'_>,
461    config: &DotGeneralConfig,
462    accumulation: DotGeneralAccumulation,
463    out: &TensorWrite<'_>,
464    op: &'static str,
465) -> crate::Result<Vec<usize>> {
466    let shape = validate_dot_general_read_into(lhs, rhs, config, out, op)?;
467    accumulation.validate_for_dtype(lhs.dtype())?;
468    Ok(shape)
469}
470
471#[doc(hidden)]
472pub fn dot_general_accum_via_temp<B: TensorDot + ?Sized>(
473    backend: &mut B,
474    lhs: TensorRead<'_>,
475    rhs: TensorRead<'_>,
476    config: &DotGeneralConfig,
477    accumulation: DotGeneralAccumulation,
478    mut out: TensorWrite<'_>,
479) -> crate::Result<()> {
480    validate_dot_general_accumulation(&lhs, &rhs, config, accumulation, &out, "dot_general")?;
481    let dot = backend.dot_general_with_conj_read(
482        lhs,
483        rhs,
484        config,
485        accumulation.lhs_conj,
486        accumulation.rhs_conj,
487    )?;
488    accumulate_dot_result_into(&dot, accumulation, &mut out)
489}
490
491fn grouped_checked_product(
492    op: &'static str,
493    role: &'static str,
494    dims: &[usize],
495) -> crate::Result<usize> {
496    dims.iter().try_fold(1usize, |acc, &dim| {
497        acc.checked_mul(dim)
498            .ok_or_else(|| crate::Error::InvalidConfig {
499                op,
500                message: format!("{role} logical element count overflows usize for shape {dims:?}"),
501            })
502    })
503}
504
505fn checked_gemm_span(
506    op: &'static str,
507    role: &'static str,
508    offset: usize,
509    rows: usize,
510    cols: usize,
511) -> crate::Result<Option<std::ops::Range<usize>>> {
512    let len = rows
513        .checked_mul(cols)
514        .ok_or_else(|| crate::Error::InvalidConfig {
515            op,
516            message: format!(
517                "{role} matrix element count overflows usize: rows={rows} cols={cols}"
518            ),
519        })?;
520    if len == 0 {
521        return Ok(None);
522    }
523    let end = offset
524        .checked_add(len)
525        .ok_or_else(|| crate::Error::InvalidConfig {
526            op,
527            message: format!("{role} matrix range overflows usize: offset={offset} len={len}"),
528        })?;
529    Ok(Some(offset..end))
530}
531
532fn validate_grouped_gemm_range(
533    op: &'static str,
534    role: &'static str,
535    len: usize,
536    range: Option<std::ops::Range<usize>>,
537) -> crate::Result<()> {
538    let Some(range) = range else {
539        return Ok(());
540    };
541    if range.end > len {
542        return Err(crate::Error::InvalidConfig {
543            op,
544            message: format!(
545                "{role} matrix range {}..{} exceeds shared buffer logical length {len}",
546                range.start, range.end
547            ),
548        });
549    }
550    Ok(())
551}
552
553#[doc(hidden)]
554pub fn validate_grouped_gemm(
555    lhs: &TensorRead<'_>,
556    rhs: &TensorRead<'_>,
557    out: &TensorWrite<'_>,
558    config: &GroupedGemmConfig<'_>,
559    op: &'static str,
560) -> crate::Result<()> {
561    if lhs.dtype() != rhs.dtype() {
562        return Err(crate::Error::DTypeMismatch {
563            op,
564            lhs: lhs.dtype(),
565            rhs: rhs.dtype(),
566        });
567    }
568    if lhs.dtype() != out.dtype() {
569        return Err(crate::Error::DTypeMismatch {
570            op,
571            lhs: lhs.dtype(),
572            rhs: out.dtype(),
573        });
574    }
575    config.accumulation.validate_for_dtype(lhs.dtype())?;
576
577    let lhs_len = grouped_checked_product(op, "lhs", lhs.shape())?;
578    let rhs_len = grouped_checked_product(op, "rhs", rhs.shape())?;
579    let out_len = grouped_checked_product(op, "out", out.shape())?;
580    // Grouped GEMM job count is runtime-controlled and can be large. Keep the
581    // validation ranges in a reserved Vec, not SmallVec, so arbitrary batches
582    // avoid inline-capacity tuning and can be sorted for O(n log n) overlap
583    // validation.
584    let mut out_ranges = Vec::<(usize, std::ops::Range<usize>)>::with_capacity(config.jobs.len());
585    for (idx, job) in config.jobs.iter().enumerate() {
586        validate_grouped_gemm_range(
587            op,
588            "lhs",
589            lhs_len,
590            checked_gemm_span(op, "lhs", job.lhs_offset, job.rows, job.contracted)?,
591        )?;
592        validate_grouped_gemm_range(
593            op,
594            "rhs",
595            rhs_len,
596            checked_gemm_span(op, "rhs", job.rhs_offset, job.contracted, job.cols)?,
597        )?;
598        let out_range = checked_gemm_span(op, "out", job.out_offset, job.rows, job.cols)?;
599        validate_grouped_gemm_range(op, "out", out_len, out_range.clone())?;
600        if let Some(out_range) = out_range {
601            out_ranges.push((idx, out_range));
602        }
603    }
604    out_ranges.sort_unstable_by_key(|(_, range)| range.start);
605    for pair in out_ranges.windows(2) {
606        let (prev_idx, previous) = &pair[0];
607        let (idx, current) = &pair[1];
608        if previous.end > current.start {
609            return Err(crate::Error::InvalidConfig {
610                op,
611                message: format!(
612                    "grouped GEMM output range for job {idx} overlaps job {prev_idx} range {}..{}",
613                    previous.start, previous.end
614                ),
615            });
616        }
617    }
618    Ok(())
619}
620
621fn add_element_offsets(
622    op: &'static str,
623    base: isize,
624    offset: usize,
625    role: &'static str,
626) -> crate::Result<isize> {
627    let offset = isize::try_from(offset).map_err(|_| crate::Error::InvalidConfig {
628        op,
629        message: format!("{role} offset {offset} does not fit in isize"),
630    })?;
631    base.checked_add(offset)
632        .ok_or_else(|| crate::Error::InvalidConfig {
633            op,
634            message: format!("{role} offset overflows isize: base={base} offset={offset}"),
635        })
636}
637
638fn dim_stride(op: &'static str, dim: usize, role: &'static str) -> crate::Result<isize> {
639    isize::try_from(dim).map_err(|_| crate::Error::InvalidConfig {
640        op,
641        message: format!("{role} leading dimension {dim} does not fit in isize"),
642    })
643}
644
645fn typed_read_storage<'a, T>(
646    tensor: &'a TypedTensor<T>,
647    op: &'static str,
648) -> crate::Result<(&'a [T], isize)> {
649    match tensor.buffer() {
650        Buffer::Host(data) => Ok((data, 0)),
651        Buffer::Backend(_) => Err(crate::Error::backend_failure(
652            op,
653            "grouped GEMM default path requires host-backed tensor storage",
654        )),
655    }
656}
657
658fn grouped_gemm_default_config() -> DotGeneralConfig {
659    // DotGeneralConfig owns Vec fields, so this rank-2 fallback config follows
660    // that API boundary rather than introducing SmallVec locally.
661    DotGeneralConfig {
662        lhs_contracting_dims: vec![1],
663        rhs_contracting_dims: vec![0],
664        lhs_batch_dims: Vec::new(),
665        rhs_batch_dims: Vec::new(),
666    }
667}
668
669trait GroupedGemmDType<T> {
670    fn wrap_read(view: TypedTensorView<'_, T>) -> TensorView<'_>;
671    fn wrap_write(view: TypedTensorViewMut<'_, T>) -> TensorViewMut<'_>;
672}
673
674struct GroupedF32;
675struct GroupedF64;
676struct GroupedC32;
677struct GroupedC64;
678
679impl GroupedGemmDType<f32> for GroupedF32 {
680    fn wrap_read(view: TypedTensorView<'_, f32>) -> TensorView<'_> {
681        TensorView::F32(view)
682    }
683
684    fn wrap_write(view: TypedTensorViewMut<'_, f32>) -> TensorViewMut<'_> {
685        TensorViewMut::F32(view)
686    }
687}
688
689impl GroupedGemmDType<f64> for GroupedF64 {
690    fn wrap_read(view: TypedTensorView<'_, f64>) -> TensorView<'_> {
691        TensorView::F64(view)
692    }
693
694    fn wrap_write(view: TypedTensorViewMut<'_, f64>) -> TensorViewMut<'_> {
695        TensorViewMut::F64(view)
696    }
697}
698
699impl GroupedGemmDType<Complex32> for GroupedC32 {
700    fn wrap_read(view: TypedTensorView<'_, Complex32>) -> TensorView<'_> {
701        TensorView::C32(view)
702    }
703
704    fn wrap_write(view: TypedTensorViewMut<'_, Complex32>) -> TensorViewMut<'_> {
705        TensorViewMut::C32(view)
706    }
707}
708
709impl GroupedGemmDType<Complex64> for GroupedC64 {
710    fn wrap_read(view: TypedTensorView<'_, Complex64>) -> TensorView<'_> {
711        TensorView::C64(view)
712    }
713
714    fn wrap_write(view: TypedTensorViewMut<'_, Complex64>) -> TensorViewMut<'_> {
715        TensorViewMut::C64(view)
716    }
717}
718
719#[allow(clippy::too_many_arguments)]
720fn grouped_gemm_default_loop<B, T, V>(
721    backend: &mut B,
722    lhs_data: &[T],
723    lhs_base: isize,
724    rhs_data: &[T],
725    rhs_base: isize,
726    out_view: &mut TypedTensorViewMut<'_, T>,
727    config: &GroupedGemmConfig<'_>,
728) -> crate::Result<()>
729where
730    B: TensorDot + ?Sized,
731    T: 'static,
732    V: GroupedGemmDType<T>,
733{
734    let op = "grouped_gemm";
735    let dot_config = grouped_gemm_default_config();
736    for job in config.jobs {
737        let lhs_offset = add_element_offsets(op, lhs_base, job.lhs_offset, "lhs")?;
738        let rhs_offset = add_element_offsets(op, rhs_base, job.rhs_offset, "rhs")?;
739        let out_offset = add_element_offsets(op, out_view.offset(), job.out_offset, "out")?;
740        let lhs_rows = dim_stride(op, job.rows, "lhs")?;
741        let rhs_rows = dim_stride(op, job.contracted, "rhs")?;
742        let out_rows = dim_stride(op, job.rows, "out")?;
743        // TypedTensorView constructors own Vec shape/stride metadata. These
744        // fallback rank-2 views are short-lived, but SmallVec is not usable
745        // without changing the view API.
746        let lhs_matrix = TypedTensorView::from_slice(
747            vec![job.rows, job.contracted],
748            vec![1, lhs_rows],
749            lhs_offset,
750            lhs_data,
751        )?;
752        let rhs_matrix = TypedTensorView::from_slice(
753            vec![job.contracted, job.cols],
754            vec![1, rhs_rows],
755            rhs_offset,
756            rhs_data,
757        )?;
758        let out_storage = out_view.host_storage_mut()?;
759        let out_matrix = TypedTensorViewMut::from_slice(
760            vec![job.rows, job.cols],
761            vec![1, out_rows],
762            out_offset,
763            out_storage,
764        )?;
765        backend.dot_general_read_into_accum(
766            TensorRead::from_view(V::wrap_read(lhs_matrix)),
767            TensorRead::from_view(V::wrap_read(rhs_matrix)),
768            &dot_config,
769            config.accumulation,
770            TensorWrite::from_view(V::wrap_write(out_matrix)),
771        )?;
772    }
773    Ok(())
774}
775
776#[doc(hidden)]
777pub fn grouped_gemm_via_sequential<B>(
778    backend: &mut B,
779    lhs: TensorRead<'_>,
780    rhs: TensorRead<'_>,
781    config: &GroupedGemmConfig<'_>,
782    mut out: TensorWrite<'_>,
783) -> crate::Result<()>
784where
785    B: TensorDot + ?Sized,
786{
787    validate_grouped_gemm(&lhs, &rhs, &out, config, "grouped_gemm")?;
788    macro_rules! dispatch {
789        ($variant:ident, $wrapper:ty) => {
790            match (&lhs, &rhs, &mut out) {
791                (
792                    TensorRead::Tensor(Tensor::$variant(a)),
793                    TensorRead::Tensor(Tensor::$variant(b)),
794                    TensorWrite::Tensor(Tensor::$variant(c)),
795                ) => {
796                    let (a_data, a_base) = typed_read_storage(a, "grouped_gemm")?;
797                    let (b_data, b_base) = typed_read_storage(b, "grouped_gemm")?;
798                    let mut c_view = c.as_view_mut();
799                    return grouped_gemm_default_loop::<_, _, $wrapper>(
800                        backend,
801                        a_data,
802                        a_base,
803                        b_data,
804                        b_base,
805                        &mut c_view,
806                        config,
807                    );
808                }
809                (
810                    TensorRead::Tensor(Tensor::$variant(a)),
811                    TensorRead::View(TensorView::$variant(b)),
812                    TensorWrite::Tensor(Tensor::$variant(c)),
813                ) => {
814                    let (a_data, a_base) = typed_read_storage(a, "grouped_gemm")?;
815                    let mut c_view = c.as_view_mut();
816                    return grouped_gemm_default_loop::<_, _, $wrapper>(
817                        backend,
818                        a_data,
819                        a_base,
820                        b.host_storage()?,
821                        b.offset(),
822                        &mut c_view,
823                        config,
824                    );
825                }
826                (
827                    TensorRead::View(TensorView::$variant(a)),
828                    TensorRead::Tensor(Tensor::$variant(b)),
829                    TensorWrite::Tensor(Tensor::$variant(c)),
830                ) => {
831                    let (b_data, b_base) = typed_read_storage(b, "grouped_gemm")?;
832                    let mut c_view = c.as_view_mut();
833                    return grouped_gemm_default_loop::<_, _, $wrapper>(
834                        backend,
835                        a.host_storage()?,
836                        a.offset(),
837                        b_data,
838                        b_base,
839                        &mut c_view,
840                        config,
841                    );
842                }
843                (
844                    TensorRead::View(TensorView::$variant(a)),
845                    TensorRead::View(TensorView::$variant(b)),
846                    TensorWrite::Tensor(Tensor::$variant(c)),
847                ) => {
848                    let mut c_view = c.as_view_mut();
849                    return grouped_gemm_default_loop::<_, _, $wrapper>(
850                        backend,
851                        a.host_storage()?,
852                        a.offset(),
853                        b.host_storage()?,
854                        b.offset(),
855                        &mut c_view,
856                        config,
857                    );
858                }
859                (
860                    TensorRead::Tensor(Tensor::$variant(a)),
861                    TensorRead::Tensor(Tensor::$variant(b)),
862                    TensorWrite::View(TensorViewMut::$variant(c)),
863                ) => {
864                    let (a_data, a_base) = typed_read_storage(a, "grouped_gemm")?;
865                    let (b_data, b_base) = typed_read_storage(b, "grouped_gemm")?;
866                    return grouped_gemm_default_loop::<_, _, $wrapper>(
867                        backend, a_data, a_base, b_data, b_base, c, config,
868                    );
869                }
870                (
871                    TensorRead::Tensor(Tensor::$variant(a)),
872                    TensorRead::View(TensorView::$variant(b)),
873                    TensorWrite::View(TensorViewMut::$variant(c)),
874                ) => {
875                    let (a_data, a_base) = typed_read_storage(a, "grouped_gemm")?;
876                    return grouped_gemm_default_loop::<_, _, $wrapper>(
877                        backend,
878                        a_data,
879                        a_base,
880                        b.host_storage()?,
881                        b.offset(),
882                        c,
883                        config,
884                    );
885                }
886                (
887                    TensorRead::View(TensorView::$variant(a)),
888                    TensorRead::Tensor(Tensor::$variant(b)),
889                    TensorWrite::View(TensorViewMut::$variant(c)),
890                ) => {
891                    let (b_data, b_base) = typed_read_storage(b, "grouped_gemm")?;
892                    return grouped_gemm_default_loop::<_, _, $wrapper>(
893                        backend,
894                        a.host_storage()?,
895                        a.offset(),
896                        b_data,
897                        b_base,
898                        c,
899                        config,
900                    );
901                }
902                (
903                    TensorRead::View(TensorView::$variant(a)),
904                    TensorRead::View(TensorView::$variant(b)),
905                    TensorWrite::View(TensorViewMut::$variant(c)),
906                ) => {
907                    return grouped_gemm_default_loop::<_, _, $wrapper>(
908                        backend,
909                        a.host_storage()?,
910                        a.offset(),
911                        b.host_storage()?,
912                        b.offset(),
913                        c,
914                        config,
915                    );
916                }
917                _ => {}
918            }
919        };
920    }
921
922    dispatch!(F32, GroupedF32);
923    dispatch!(F64, GroupedF64);
924    dispatch!(C32, GroupedC32);
925    dispatch!(C64, GroupedC64);
926    Err(crate::Error::DTypeMismatch {
927        op: "grouped_gemm",
928        lhs: lhs.dtype(),
929        rhs: out.dtype(),
930    })
931}
932
933fn grouped_gemm_default<B>(
934    backend: &mut B,
935    lhs: TensorRead<'_>,
936    rhs: TensorRead<'_>,
937    config: &GroupedGemmConfig<'_>,
938    out: TensorWrite<'_>,
939) -> crate::Result<()>
940where
941    B: TensorDot + ?Sized,
942{
943    grouped_gemm_via_sequential(backend, lhs, rhs, config, out)
944}
945
946#[doc(hidden)]
947pub fn accumulate_dot_result_into(
948    dot: &Tensor,
949    accumulation: DotGeneralAccumulation,
950    out: &mut TensorWrite<'_>,
951) -> crate::Result<()> {
952    macro_rules! dispatch {
953        ($variant:ident, $ty:ty) => {
954            if let (
955                Tensor::$variant(dot),
956                ContractionScalar::$variant(alpha),
957                ContractionScalar::$variant(beta),
958            ) = (dot, accumulation.alpha, accumulation.beta)
959            {
960                match out {
961                    TensorWrite::Tensor(Tensor::$variant(out)) => {
962                        let mut out = out.as_view_mut();
963                        accumulate_typed(dot.as_slice()?, alpha, beta, &mut out)?;
964                        return Ok(());
965                    }
966                    TensorWrite::View(crate::TensorViewMut::$variant(out)) => {
967                        accumulate_typed(dot.as_slice()?, alpha, beta, out)?;
968                        return Ok(());
969                    }
970                    _ => {}
971                }
972            }
973        };
974    }
975
976    dispatch!(F32, f32);
977    dispatch!(F64, f64);
978    dispatch!(C32, Complex32);
979    dispatch!(C64, Complex64);
980
981    Err(crate::Error::DTypeMismatch {
982        op: "dot_general",
983        lhs: accumulation.alpha.dtype(),
984        rhs: dot.dtype(),
985    })
986}
987
988fn accumulate_typed<T>(
989    dot: &[T],
990    alpha: T,
991    beta: T,
992    out: &mut TypedTensorViewMut<'_, T>,
993) -> crate::Result<()>
994where
995    T: Copy
996        + PartialEq
997        + std::ops::Add<Output = T>
998        + std::ops::Mul<Output = T>
999        + num_traits::Zero
1000        + 'static,
1001{
1002    let beta_is_zero = beta == T::zero();
1003    for (linear, dot_value) in dot.iter().copied().enumerate() {
1004        let indices = flat_to_multi_for_shape(out.shape(), linear);
1005        let output = out
1006            .get_mut(&indices)
1007            .ok_or_else(|| crate::Error::InvalidConfig {
1008                op: "dot_general",
1009                message: format!("output index {indices:?} is outside accumulation target"),
1010            })?;
1011        // INVARIANT: beta == 0 follows BLAS GEMM semantics and does not read
1012        // the existing output element; beta != 0 requires an initialized
1013        // TensorWrite target and performs a read-modify-write update.
1014        *output = if beta_is_zero {
1015            alpha * dot_value
1016        } else {
1017            alpha * dot_value + beta * *output
1018        };
1019    }
1020    Ok(())
1021}
1022
1023fn flat_to_multi_for_shape(shape: &[usize], mut linear: usize) -> Vec<usize> {
1024    let mut indices = Vec::with_capacity(shape.len());
1025    for &dim in shape {
1026        if dim == 0 {
1027            indices.push(0);
1028        } else {
1029            indices.push(linear % dim);
1030            linear /= dim;
1031        }
1032    }
1033    indices
1034}
1035
1036/// Canonical elementwise fusion plan shared between segmented execution and backends.
1037#[doc(hidden)]
1038#[derive(Clone, Debug, Hash, PartialEq, Eq)]
1039pub struct ElementwiseFusionPlan {
1040    dtype: crate::DType,
1041    input_count: usize,
1042    // Keep view metadata in Vecs. A/B benchmarking on the broadcast_mul
1043    // path showed SmallVec made this metadata path about 6-7% slower.
1044    input_views: Vec<ElementwiseFusionInputView>,
1045    outputs: Vec<usize>,
1046    ops: Vec<ElementwiseFusionInst>,
1047}
1048
1049/// Metadata-only view applied to one backend fusion input.
1050#[doc(hidden)]
1051#[derive(Clone, Debug, Hash, PartialEq, Eq)]
1052pub enum ElementwiseFusionInputView {
1053    Identity,
1054    BroadcastInDim {
1055        // Vec is intentional here; see ElementwiseFusionPlan::input_views.
1056        shape: Vec<usize>,
1057        dims: Vec<usize>,
1058    },
1059}
1060
1061/// One node in a canonical elementwise fusion plan.
1062#[doc(hidden)]
1063#[derive(Clone, Debug, Hash, PartialEq, Eq)]
1064pub struct ElementwiseFusionInst {
1065    op: ElementwiseFusionOp,
1066    inputs: Vec<usize>,
1067}
1068
1069tenferro_core_ops::define_elementwise_fusion_op!();
1070
1071impl ElementwiseFusionPlan {
1072    /// Build a backend elementwise fusion plan.
1073    ///
1074    /// # Examples
1075    ///
1076    /// ```rust
1077    /// use tenferro_tensor::backend::{
1078    ///     ElementwiseFusionInst, ElementwiseFusionOp, ElementwiseFusionPlan,
1079    /// };
1080    /// use tenferro_tensor::DType;
1081    ///
1082    /// let plan = ElementwiseFusionPlan::new(
1083    ///     DType::F64,
1084    ///     2,
1085    ///     vec![2],
1086    ///     vec![ElementwiseFusionInst::new(ElementwiseFusionOp::Add, vec![0, 1])],
1087    /// );
1088    /// assert_eq!(plan.input_count(), 2);
1089    /// ```
1090    pub fn new(
1091        dtype: crate::DType,
1092        input_count: usize,
1093        outputs: Vec<usize>,
1094        ops: Vec<ElementwiseFusionInst>,
1095    ) -> Self {
1096        Self::with_input_views(
1097            dtype,
1098            vec![ElementwiseFusionInputView::Identity; input_count],
1099            outputs,
1100            ops,
1101        )
1102    }
1103
1104    /// Build a backend elementwise fusion plan with input view metadata.
1105    ///
1106    /// # Examples
1107    ///
1108    /// ```rust
1109    /// use tenferro_tensor::backend::{
1110    ///     ElementwiseFusionInputView, ElementwiseFusionInst, ElementwiseFusionOp,
1111    ///     ElementwiseFusionPlan,
1112    /// };
1113    /// use tenferro_tensor::DType;
1114    ///
1115    /// let plan = ElementwiseFusionPlan::with_input_views(
1116    ///     DType::F64,
1117    ///     vec![ElementwiseFusionInputView::broadcast_in_dim(vec![2, 3], vec![0])],
1118    ///     vec![1],
1119    ///     vec![ElementwiseFusionInst::new(ElementwiseFusionOp::Negate, vec![0])],
1120    /// );
1121    /// assert_eq!(plan.input_count(), 1);
1122    /// ```
1123    pub fn with_input_views(
1124        dtype: crate::DType,
1125        input_views: impl IntoIterator<Item = ElementwiseFusionInputView>,
1126        outputs: Vec<usize>,
1127        ops: Vec<ElementwiseFusionInst>,
1128    ) -> Self {
1129        let input_views = input_views.into_iter().collect::<Vec<_>>();
1130        let input_count = input_views.len();
1131        Self {
1132            dtype,
1133            input_count,
1134            input_views,
1135            outputs,
1136            ops,
1137        }
1138    }
1139
1140    /// Return the scalar dtype expected by this fusion plan.
1141    ///
1142    /// # Examples
1143    ///
1144    /// ```rust
1145    /// use tenferro_tensor::backend::ElementwiseFusionPlan;
1146    /// use tenferro_tensor::DType;
1147    ///
1148    /// let plan = ElementwiseFusionPlan::new(DType::F32, 0, Vec::new(), Vec::new());
1149    /// assert_eq!(plan.dtype(), DType::F32);
1150    /// ```
1151    pub fn dtype(&self) -> crate::DType {
1152        self.dtype
1153    }
1154
1155    /// Return the number of input tensors expected by this plan.
1156    ///
1157    /// # Examples
1158    ///
1159    /// ```rust
1160    /// use tenferro_tensor::backend::ElementwiseFusionPlan;
1161    /// use tenferro_tensor::DType;
1162    ///
1163    /// let plan = ElementwiseFusionPlan::new(DType::F64, 3, Vec::new(), Vec::new());
1164    /// assert_eq!(plan.input_count(), 3);
1165    /// ```
1166    pub fn input_count(&self) -> usize {
1167        self.input_count
1168    }
1169
1170    /// Return metadata views applied to fusion inputs before executing ops.
1171    ///
1172    /// # Examples
1173    ///
1174    /// ```rust
1175    /// use tenferro_tensor::backend::ElementwiseFusionPlan;
1176    /// use tenferro_tensor::DType;
1177    ///
1178    /// let plan = ElementwiseFusionPlan::new(DType::F64, 2, Vec::new(), Vec::new());
1179    /// assert_eq!(plan.input_views().len(), 2);
1180    /// ```
1181    pub fn input_views(&self) -> &[ElementwiseFusionInputView] {
1182        &self.input_views
1183    }
1184
1185    /// Return the value ids selected as fusion outputs.
1186    ///
1187    /// # Examples
1188    ///
1189    /// ```rust
1190    /// use tenferro_tensor::backend::ElementwiseFusionPlan;
1191    /// use tenferro_tensor::DType;
1192    ///
1193    /// let plan = ElementwiseFusionPlan::new(DType::F64, 0, vec![0], Vec::new());
1194    /// assert_eq!(plan.outputs(), &[0]);
1195    /// ```
1196    pub fn outputs(&self) -> &[usize] {
1197        &self.outputs
1198    }
1199
1200    /// Return the fused elementwise instruction sequence.
1201    ///
1202    /// # Examples
1203    ///
1204    /// ```rust
1205    /// use tenferro_tensor::backend::{
1206    ///     ElementwiseFusionInst, ElementwiseFusionOp, ElementwiseFusionPlan,
1207    /// };
1208    /// use tenferro_tensor::DType;
1209    ///
1210    /// let inst = ElementwiseFusionInst::new(ElementwiseFusionOp::Negate, vec![0]);
1211    /// let plan = ElementwiseFusionPlan::new(DType::F64, 1, vec![1], vec![inst]);
1212    /// assert_eq!(plan.ops().len(), 1);
1213    /// ```
1214    pub fn ops(&self) -> &[ElementwiseFusionInst] {
1215        &self.ops
1216    }
1217}
1218
1219impl ElementwiseFusionInputView {
1220    /// Build metadata for a `BroadcastInDim` fusion input view.
1221    ///
1222    /// # Examples
1223    ///
1224    /// ```rust
1225    /// use tenferro_tensor::backend::ElementwiseFusionInputView;
1226    ///
1227    /// let view = ElementwiseFusionInputView::broadcast_in_dim(vec![2, 3], vec![0]);
1228    /// assert!(matches!(view, ElementwiseFusionInputView::BroadcastInDim { .. }));
1229    /// ```
1230    pub fn broadcast_in_dim(
1231        shape: impl IntoIterator<Item = usize>,
1232        dims: impl IntoIterator<Item = usize>,
1233    ) -> Self {
1234        Self::BroadcastInDim {
1235            shape: shape.into_iter().collect(),
1236            dims: dims.into_iter().collect(),
1237        }
1238    }
1239
1240    /// Return true when this fusion input is an identity view.
1241    ///
1242    /// # Examples
1243    ///
1244    /// ```rust
1245    /// use tenferro_tensor::backend::ElementwiseFusionInputView;
1246    ///
1247    /// assert!(ElementwiseFusionInputView::Identity.is_identity());
1248    /// ```
1249    pub fn is_identity(&self) -> bool {
1250        matches!(self, Self::Identity)
1251    }
1252}
1253
1254impl ElementwiseFusionInst {
1255    /// Build a backend elementwise fusion instruction.
1256    ///
1257    /// # Examples
1258    ///
1259    /// ```rust
1260    /// use tenferro_tensor::backend::{ElementwiseFusionInst, ElementwiseFusionOp};
1261    ///
1262    /// let inst = ElementwiseFusionInst::new(ElementwiseFusionOp::Add, vec![0, 1]);
1263    /// assert_eq!(inst.inputs(), &[0, 1]);
1264    /// ```
1265    pub fn new(op: ElementwiseFusionOp, inputs: Vec<usize>) -> Self {
1266        Self { op, inputs }
1267    }
1268
1269    /// Return the elementwise op executed by this instruction.
1270    ///
1271    /// # Examples
1272    ///
1273    /// ```rust
1274    /// use tenferro_tensor::backend::{ElementwiseFusionInst, ElementwiseFusionOp};
1275    ///
1276    /// let inst = ElementwiseFusionInst::new(ElementwiseFusionOp::Negate, vec![0]);
1277    /// assert_eq!(inst.op(), ElementwiseFusionOp::Negate);
1278    /// ```
1279    pub fn op(&self) -> ElementwiseFusionOp {
1280        self.op
1281    }
1282
1283    /// Return this instruction's input value ids.
1284    ///
1285    /// # Examples
1286    ///
1287    /// ```rust
1288    /// use tenferro_tensor::backend::{ElementwiseFusionInst, ElementwiseFusionOp};
1289    ///
1290    /// let inst = ElementwiseFusionInst::new(ElementwiseFusionOp::Multiply, vec![2, 0]);
1291    /// assert_eq!(inst.inputs(), &[2, 0]);
1292    /// ```
1293    pub fn inputs(&self) -> &[usize] {
1294        &self.inputs
1295    }
1296}
1297
1298/// Elementwise tensor operations.
1299///
1300/// # Examples
1301///
1302/// ```rust
1303/// use tenferro_tensor::TensorElementwise;
1304///
1305/// fn accepts_elementwise<B: TensorElementwise>(_backend: &mut B) {}
1306/// ```
1307pub trait TensorElementwise {
1308    fn add(&mut self, lhs: &Tensor, rhs: &Tensor) -> crate::Result<Tensor>;
1309
1310    /// Elementwise addition accepting either owned tensors or borrowed views.
1311    ///
1312    /// Backends that implement this method must not silently move data across
1313    /// devices. A backend that cannot consume views should return an explicit
1314    /// backend error rather than materializing or transferring implicitly.
1315    ///
1316    /// # Examples
1317    ///
1318    /// ```rust
1319    /// use tenferro_tensor::{Tensor, TensorElementwise, TensorRead};
1320    ///
1321    /// fn add_owned<B: TensorElementwise>(
1322    ///     backend: &mut B,
1323    ///     lhs: &Tensor,
1324    ///     rhs: &Tensor,
1325    /// ) -> tenferro_tensor::Result<Tensor> {
1326    ///     backend.add_read(TensorRead::from_tensor(lhs), TensorRead::from_tensor(rhs))
1327    /// }
1328    /// ```
1329    fn add_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
1330        self.add(read_tensor("add", lhs)?, read_tensor("add", rhs)?)
1331    }
1332
1333    /// Overwrite caller-provided output with elementwise addition.
1334    ///
1335    /// `_into` methods never accumulate into the previous output value.
1336    ///
1337    /// # Examples
1338    ///
1339    /// ```rust
1340    /// use tenferro_tensor::{Tensor, TensorElementwise, TensorWrite};
1341    ///
1342    /// fn add_into<B: TensorElementwise>(
1343    ///     backend: &mut B,
1344    ///     lhs: &Tensor,
1345    ///     rhs: &Tensor,
1346    ///     mut out: Tensor,
1347    /// ) -> tenferro_tensor::Result<Tensor> {
1348    ///     backend.add_into(lhs, rhs, TensorWrite::from_tensor(&mut out))?;
1349    ///     Ok(out)
1350    /// }
1351    /// ```
1352    fn add_into(&mut self, lhs: &Tensor, rhs: &Tensor, out: TensorWrite<'_>) -> crate::Result<()> {
1353        self.add_read_into(
1354            TensorRead::from_tensor(lhs),
1355            TensorRead::from_tensor(rhs),
1356            out,
1357        )
1358    }
1359
1360    /// Overwrite caller-provided output with elementwise addition from reads.
1361    ///
1362    /// # Examples
1363    ///
1364    /// ```rust
1365    /// use tenferro_tensor::{TensorElementwise, TensorRead, TensorWrite};
1366    ///
1367    /// fn add_read_into<B: TensorElementwise>(
1368    ///     backend: &mut B,
1369    ///     lhs: TensorRead<'_>,
1370    ///     rhs: TensorRead<'_>,
1371    ///     out: TensorWrite<'_>,
1372    /// ) -> tenferro_tensor::Result<()> {
1373    ///     backend.add_read_into(lhs, rhs, out)
1374    /// }
1375    /// ```
1376    fn add_read_into(
1377        &mut self,
1378        lhs: TensorRead<'_>,
1379        rhs: TensorRead<'_>,
1380        mut out: TensorWrite<'_>,
1381    ) -> crate::Result<()> {
1382        let result = self.add_read(lhs, rhs)?;
1383        out.copy_from_tensor(&result)
1384    }
1385
1386    fn sub(&mut self, lhs: &Tensor, rhs: &Tensor) -> crate::Result<Tensor>;
1387
1388    /// Elementwise subtraction accepting either owned tensors or borrowed views.
1389    ///
1390    /// # Examples
1391    ///
1392    /// ```rust
1393    /// use tenferro_tensor::{Tensor, TensorElementwise, TensorRead};
1394    ///
1395    /// fn sub_owned<B: TensorElementwise>(
1396    ///     backend: &mut B,
1397    ///     lhs: &Tensor,
1398    ///     rhs: &Tensor,
1399    /// ) -> tenferro_tensor::Result<Tensor> {
1400    ///     backend.sub_read(TensorRead::from_tensor(lhs), TensorRead::from_tensor(rhs))
1401    /// }
1402    /// ```
1403    fn sub_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
1404        self.sub(read_tensor("sub", lhs)?, read_tensor("sub", rhs)?)
1405    }
1406
1407    /// Overwrite caller-provided output with elementwise subtraction.
1408    fn sub_into(&mut self, lhs: &Tensor, rhs: &Tensor, out: TensorWrite<'_>) -> crate::Result<()> {
1409        self.sub_read_into(
1410            TensorRead::from_tensor(lhs),
1411            TensorRead::from_tensor(rhs),
1412            out,
1413        )
1414    }
1415
1416    /// Overwrite caller-provided output with elementwise subtraction from reads.
1417    fn sub_read_into(
1418        &mut self,
1419        lhs: TensorRead<'_>,
1420        rhs: TensorRead<'_>,
1421        mut out: TensorWrite<'_>,
1422    ) -> crate::Result<()> {
1423        let result = self.sub_read(lhs, rhs)?;
1424        out.copy_from_tensor(&result)
1425    }
1426
1427    fn mul(&mut self, lhs: &Tensor, rhs: &Tensor) -> crate::Result<Tensor>;
1428    fn mul_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
1429        self.mul(read_tensor("mul", lhs)?, read_tensor("mul", rhs)?)
1430    }
1431
1432    /// Overwrite caller-provided output with elementwise multiplication.
1433    fn mul_into(&mut self, lhs: &Tensor, rhs: &Tensor, out: TensorWrite<'_>) -> crate::Result<()> {
1434        self.mul_read_into(
1435            TensorRead::from_tensor(lhs),
1436            TensorRead::from_tensor(rhs),
1437            out,
1438        )
1439    }
1440
1441    /// Overwrite caller-provided output with elementwise multiplication from reads.
1442    fn mul_read_into(
1443        &mut self,
1444        lhs: TensorRead<'_>,
1445        rhs: TensorRead<'_>,
1446        mut out: TensorWrite<'_>,
1447    ) -> crate::Result<()> {
1448        let result = self.mul_read(lhs, rhs)?;
1449        out.copy_from_tensor(&result)
1450    }
1451
1452    fn neg(&mut self, input: &Tensor) -> crate::Result<Tensor>;
1453    fn neg_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
1454        self.neg(read_tensor("neg", input)?)
1455    }
1456
1457    /// Overwrite caller-provided output with elementwise negation.
1458    fn neg_into(&mut self, input: &Tensor, out: TensorWrite<'_>) -> crate::Result<()> {
1459        self.neg_read_into(TensorRead::from_tensor(input), out)
1460    }
1461
1462    /// Overwrite caller-provided output with elementwise negation from a read.
1463    fn neg_read_into(
1464        &mut self,
1465        input: TensorRead<'_>,
1466        mut out: TensorWrite<'_>,
1467    ) -> crate::Result<()> {
1468        let result = self.neg_read(input)?;
1469        out.copy_from_tensor(&result)
1470    }
1471
1472    fn conj(&mut self, input: &Tensor) -> crate::Result<Tensor>;
1473    fn conj_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
1474        self.conj(read_tensor("conj", input)?)
1475    }
1476
1477    /// Overwrite caller-provided output with elementwise conjugation.
1478    fn conj_into(&mut self, input: &Tensor, out: TensorWrite<'_>) -> crate::Result<()> {
1479        self.conj_read_into(TensorRead::from_tensor(input), out)
1480    }
1481
1482    /// Overwrite caller-provided output with elementwise conjugation from a read.
1483    fn conj_read_into(
1484        &mut self,
1485        input: TensorRead<'_>,
1486        mut out: TensorWrite<'_>,
1487    ) -> crate::Result<()> {
1488        let result = self.conj_read(input)?;
1489        out.copy_from_tensor(&result)
1490    }
1491
1492    fn div(&mut self, lhs: &Tensor, rhs: &Tensor) -> crate::Result<Tensor>;
1493    fn div_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
1494        self.div(read_tensor("div", lhs)?, read_tensor("div", rhs)?)
1495    }
1496
1497    /// Overwrite caller-provided output with elementwise division.
1498    fn div_into(&mut self, lhs: &Tensor, rhs: &Tensor, out: TensorWrite<'_>) -> crate::Result<()> {
1499        self.div_read_into(
1500            TensorRead::from_tensor(lhs),
1501            TensorRead::from_tensor(rhs),
1502            out,
1503        )
1504    }
1505
1506    /// Overwrite caller-provided output with elementwise division from reads.
1507    fn div_read_into(
1508        &mut self,
1509        lhs: TensorRead<'_>,
1510        rhs: TensorRead<'_>,
1511        mut out: TensorWrite<'_>,
1512    ) -> crate::Result<()> {
1513        let result = self.div_read(lhs, rhs)?;
1514        out.copy_from_tensor(&result)
1515    }
1516
1517    /// Elementwise remainder.
1518    ///
1519    /// The default is an explicit unsupported error so backend implementors can
1520    /// opt in without silent fallback.
1521    ///
1522    /// # Examples
1523    ///
1524    /// ```rust
1525    /// use tenferro_tensor::{Tensor, TensorElementwise};
1526    ///
1527    /// fn rem_owned<B: TensorElementwise>(
1528    ///     backend: &mut B,
1529    ///     lhs: &Tensor,
1530    ///     rhs: &Tensor,
1531    /// ) -> tenferro_tensor::Result<Tensor> {
1532    ///     backend.rem(lhs, rhs)
1533    /// }
1534    /// ```
1535    fn rem(&mut self, lhs: &Tensor, _rhs: &Tensor) -> crate::Result<Tensor> {
1536        Err(crate::Error::backend_failure(
1537            "rem",
1538            format!("backend does not implement rem for dtype {:?}", lhs.dtype()),
1539        ))
1540    }
1541
1542    /// Elementwise remainder accepting owned tensors or borrowed views.
1543    ///
1544    /// # Examples
1545    ///
1546    /// ```rust
1547    /// use tenferro_tensor::{Tensor, TensorElementwise, TensorRead};
1548    ///
1549    /// fn rem_read<B: TensorElementwise>(
1550    ///     backend: &mut B,
1551    ///     lhs: &Tensor,
1552    ///     rhs: &Tensor,
1553    /// ) -> tenferro_tensor::Result<Tensor> {
1554    ///     backend.rem_read(TensorRead::from_tensor(lhs), TensorRead::from_tensor(rhs))
1555    /// }
1556    /// ```
1557    fn rem_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
1558        self.rem(read_tensor("rem", lhs)?, read_tensor("rem", rhs)?)
1559    }
1560
1561    fn abs(&mut self, input: &Tensor) -> crate::Result<Tensor>;
1562    fn abs_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
1563        self.abs(read_tensor("abs", input)?)
1564    }
1565
1566    fn sign(&mut self, input: &Tensor) -> crate::Result<Tensor>;
1567    fn sign_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
1568        self.sign(read_tensor("sign", input)?)
1569    }
1570
1571    fn maximum(&mut self, lhs: &Tensor, rhs: &Tensor) -> crate::Result<Tensor>;
1572    fn maximum_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
1573        self.maximum(read_tensor("maximum", lhs)?, read_tensor("maximum", rhs)?)
1574    }
1575
1576    fn minimum(&mut self, lhs: &Tensor, rhs: &Tensor) -> crate::Result<Tensor>;
1577    fn minimum_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
1578        self.minimum(read_tensor("minimum", lhs)?, read_tensor("minimum", rhs)?)
1579    }
1580
1581    fn compare(&mut self, lhs: &Tensor, rhs: &Tensor, dir: &CompareDir) -> crate::Result<Tensor>;
1582    fn compare_read(
1583        &mut self,
1584        lhs: TensorRead<'_>,
1585        rhs: TensorRead<'_>,
1586        dir: &CompareDir,
1587    ) -> crate::Result<Tensor> {
1588        self.compare(
1589            read_tensor("compare", lhs)?,
1590            read_tensor("compare", rhs)?,
1591            dir,
1592        )
1593    }
1594
1595    fn select(
1596        &mut self,
1597        pred: &Tensor,
1598        on_true: &Tensor,
1599        on_false: &Tensor,
1600    ) -> crate::Result<Tensor>;
1601    fn select_read(
1602        &mut self,
1603        pred: TensorRead<'_>,
1604        on_true: TensorRead<'_>,
1605        on_false: TensorRead<'_>,
1606    ) -> crate::Result<Tensor> {
1607        self.select(
1608            read_tensor("select", pred)?,
1609            read_tensor("select", on_true)?,
1610            read_tensor("select", on_false)?,
1611        )
1612    }
1613
1614    fn clamp(&mut self, input: &Tensor, lower: &Tensor, upper: &Tensor) -> crate::Result<Tensor>;
1615    fn clamp_read(
1616        &mut self,
1617        input: TensorRead<'_>,
1618        lower: TensorRead<'_>,
1619        upper: TensorRead<'_>,
1620    ) -> crate::Result<Tensor> {
1621        self.clamp(
1622            read_tensor("clamp", input)?,
1623            read_tensor("clamp", lower)?,
1624            read_tensor("clamp", upper)?,
1625        )
1626    }
1627}
1628
1629/// Analytic unary and binary tensor operations.
1630///
1631/// # Examples
1632///
1633/// ```rust
1634/// use tenferro_tensor::TensorAnalytic;
1635///
1636/// fn accepts_analytic<B: TensorAnalytic>(_backend: &mut B) {}
1637/// ```
1638pub trait TensorAnalytic {
1639    fn exp(&mut self, input: &Tensor) -> crate::Result<Tensor>;
1640    fn exp_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
1641        self.exp(read_tensor("exp", input)?)
1642    }
1643
1644    fn log(&mut self, input: &Tensor) -> crate::Result<Tensor>;
1645    fn log_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
1646        self.log(read_tensor("log", input)?)
1647    }
1648
1649    fn sin(&mut self, input: &Tensor) -> crate::Result<Tensor>;
1650    fn sin_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
1651        self.sin(read_tensor("sin", input)?)
1652    }
1653
1654    fn cos(&mut self, input: &Tensor) -> crate::Result<Tensor>;
1655    fn cos_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
1656        self.cos(read_tensor("cos", input)?)
1657    }
1658
1659    fn tanh(&mut self, input: &Tensor) -> crate::Result<Tensor>;
1660    fn tanh_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
1661        self.tanh(read_tensor("tanh", input)?)
1662    }
1663
1664    fn sqrt(&mut self, input: &Tensor) -> crate::Result<Tensor>;
1665    fn sqrt_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
1666        self.sqrt(read_tensor("sqrt", input)?)
1667    }
1668
1669    fn rsqrt(&mut self, input: &Tensor) -> crate::Result<Tensor>;
1670    fn rsqrt_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
1671        self.rsqrt(read_tensor("rsqrt", input)?)
1672    }
1673
1674    fn pow(&mut self, lhs: &Tensor, rhs: &Tensor) -> crate::Result<Tensor>;
1675    fn pow_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
1676        self.pow(read_tensor("pow", lhs)?, read_tensor("pow", rhs)?)
1677    }
1678
1679    fn expm1(&mut self, input: &Tensor) -> crate::Result<Tensor>;
1680    fn expm1_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
1681        self.expm1(read_tensor("expm1", input)?)
1682    }
1683
1684    fn log1p(&mut self, input: &Tensor) -> crate::Result<Tensor>;
1685    fn log1p_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
1686        self.log1p(read_tensor("log1p", input)?)
1687    }
1688}
1689
1690/// Shape, layout, and dtype transformation operations.
1691///
1692/// # Examples
1693///
1694/// ```rust
1695/// use tenferro_tensor::TensorStructural;
1696///
1697/// fn accepts_structural<B: TensorStructural>(_backend: &mut B) {}
1698/// ```
1699pub trait TensorStructural {
1700    fn transpose(&mut self, input: &Tensor, perm: &[usize]) -> crate::Result<Tensor>;
1701    fn transpose_read(&mut self, input: TensorRead<'_>, perm: &[usize]) -> crate::Result<Tensor> {
1702        self.transpose(read_tensor("transpose", input)?, perm)
1703    }
1704
1705    fn reshape(&mut self, input: &Tensor, shape: &[usize]) -> crate::Result<Tensor>;
1706    fn reshape_read(&mut self, input: TensorRead<'_>, shape: &[usize]) -> crate::Result<Tensor> {
1707        self.reshape(read_tensor("reshape", input)?, shape)
1708    }
1709
1710    fn broadcast_in_dim(
1711        &mut self,
1712        input: &Tensor,
1713        shape: &[usize],
1714        dims: &[usize],
1715    ) -> crate::Result<Tensor>;
1716    fn broadcast_in_dim_read(
1717        &mut self,
1718        input: TensorRead<'_>,
1719        shape: &[usize],
1720        dims: &[usize],
1721    ) -> crate::Result<Tensor> {
1722        self.broadcast_in_dim(read_tensor("broadcast_in_dim", input)?, shape, dims)
1723    }
1724
1725    /// Cast a tensor to another dtype using explicit dtype projection.
1726    ///
1727    /// Backends may truncate, narrow precision, project complex values, or use
1728    /// boolean truthiness according to their documented cast support.
1729    ///
1730    /// # Examples
1731    ///
1732    /// ```rust
1733    /// use tenferro_tensor::{DType, Tensor, TensorStructural};
1734    ///
1735    /// fn cast_to_i32<B: TensorStructural>(
1736    ///     backend: &mut B,
1737    ///     input: &Tensor,
1738    /// ) -> tenferro_tensor::Result<Tensor> {
1739    ///     backend.cast(input, DType::I32)
1740    /// }
1741    /// ```
1742    fn cast(&mut self, input: &Tensor, to: crate::DType) -> crate::Result<Tensor>;
1743
1744    /// Convert a tensor to another dtype using checked dtype conversion.
1745    ///
1746    /// `convert` accepts only conversions allowed by tenferro's dtype-promotion
1747    /// lattice. Use [`TensorStructural::cast`] for explicit lossy projection.
1748    ///
1749    /// # Examples
1750    ///
1751    /// ```rust
1752    /// use tenferro_tensor::{DType, Tensor, TensorStructural};
1753    ///
1754    /// fn convert_to_f64<B: TensorStructural>(
1755    ///     backend: &mut B,
1756    ///     input: &Tensor,
1757    /// ) -> tenferro_tensor::Result<Tensor> {
1758    ///     backend.convert(input, DType::F64)
1759    /// }
1760    /// ```
1761    fn convert(&mut self, input: &Tensor, to: crate::DType) -> crate::Result<Tensor> {
1762        validate_convert_dtype("convert", input.dtype(), to)?;
1763        self.cast(input, to)
1764    }
1765
1766    fn extract_diagonal(
1767        &mut self,
1768        input: &Tensor,
1769        axis_a: usize,
1770        axis_b: usize,
1771    ) -> crate::Result<Tensor>;
1772    fn embed_diagonal(
1773        &mut self,
1774        input: &Tensor,
1775        axis_a: usize,
1776        axis_b: usize,
1777    ) -> crate::Result<Tensor>;
1778    fn tril(&mut self, input: &Tensor, k: i64) -> crate::Result<Tensor>;
1779    fn triu(&mut self, input: &Tensor, k: i64) -> crate::Result<Tensor>;
1780}
1781
1782/// Reduction operations.
1783///
1784/// Reducing over an axis whose extent is zero returns an error for every
1785/// reduction operation. Passing an empty `axes` slice is a no-op and returns the
1786/// input values unchanged.
1787///
1788/// # Examples
1789///
1790/// ```rust
1791/// use tenferro_tensor::TensorReduction;
1792///
1793/// fn accepts_reduction<B: TensorReduction>(_backend: &mut B) {}
1794/// ```
1795pub trait TensorReduction {
1796    fn reduce_sum(&mut self, input: &Tensor, axes: &[usize]) -> crate::Result<Tensor>;
1797
1798    /// Sum elements across axes from an owned tensor or borrowed view.
1799    ///
1800    /// # Examples
1801    ///
1802    /// ```rust
1803    /// use tenferro_tensor::{Tensor, TensorRead, TensorReduction};
1804    ///
1805    /// fn sum_owned<B: TensorReduction>(
1806    ///     backend: &mut B,
1807    ///     input: &Tensor,
1808    /// ) -> tenferro_tensor::Result<Tensor> {
1809    ///     backend.reduce_sum_read(TensorRead::from_tensor(input), &[0])
1810    /// }
1811    /// ```
1812    fn reduce_sum_read(&mut self, input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
1813        match input.as_tensor() {
1814            Some(input) => self.reduce_sum(input, axes),
1815            None => Err(crate::Error::backend_failure(
1816                "reduce_sum",
1817                "backend does not accept borrowed tensor views at this execution boundary",
1818            )),
1819        }
1820    }
1821
1822    fn reduce_prod(&mut self, input: &Tensor, axes: &[usize]) -> crate::Result<Tensor>;
1823
1824    /// Multiply elements across axes from an owned tensor or borrowed view.
1825    ///
1826    /// # Examples
1827    ///
1828    /// ```rust
1829    /// use tenferro_tensor::{Tensor, TensorRead, TensorReduction};
1830    ///
1831    /// fn prod_owned<B: TensorReduction>(
1832    ///     backend: &mut B,
1833    ///     input: &Tensor,
1834    /// ) -> tenferro_tensor::Result<Tensor> {
1835    ///     backend.reduce_prod_read(TensorRead::from_tensor(input), &[0])
1836    /// }
1837    /// ```
1838    fn reduce_prod_read(&mut self, input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
1839        match input.as_tensor() {
1840            Some(input) => self.reduce_prod(input, axes),
1841            None => Err(crate::Error::backend_failure(
1842                "reduce_prod",
1843                "backend does not accept borrowed tensor views at this execution boundary",
1844            )),
1845        }
1846    }
1847
1848    fn reduce_max(&mut self, input: &Tensor, axes: &[usize]) -> crate::Result<Tensor>;
1849
1850    /// Take maximum values across axes from an owned tensor or borrowed view.
1851    ///
1852    /// # Examples
1853    ///
1854    /// ```rust
1855    /// use tenferro_tensor::{Tensor, TensorRead, TensorReduction};
1856    ///
1857    /// fn max_owned<B: TensorReduction>(
1858    ///     backend: &mut B,
1859    ///     input: &Tensor,
1860    /// ) -> tenferro_tensor::Result<Tensor> {
1861    ///     backend.reduce_max_read(TensorRead::from_tensor(input), &[0])
1862    /// }
1863    /// ```
1864    fn reduce_max_read(&mut self, input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
1865        match input.as_tensor() {
1866            Some(input) => self.reduce_max(input, axes),
1867            None => Err(crate::Error::backend_failure(
1868                "reduce_max",
1869                "backend does not accept borrowed tensor views at this execution boundary",
1870            )),
1871        }
1872    }
1873
1874    fn reduce_min(&mut self, input: &Tensor, axes: &[usize]) -> crate::Result<Tensor>;
1875
1876    /// Take minimum values across axes from an owned tensor or borrowed view.
1877    ///
1878    /// # Examples
1879    ///
1880    /// ```rust
1881    /// use tenferro_tensor::{Tensor, TensorRead, TensorReduction};
1882    ///
1883    /// fn min_owned<B: TensorReduction>(
1884    ///     backend: &mut B,
1885    ///     input: &Tensor,
1886    /// ) -> tenferro_tensor::Result<Tensor> {
1887    ///     backend.reduce_min_read(TensorRead::from_tensor(input), &[0])
1888    /// }
1889    /// ```
1890    fn reduce_min_read(&mut self, input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
1891        match input.as_tensor() {
1892            Some(input) => self.reduce_min(input, axes),
1893            None => Err(crate::Error::backend_failure(
1894                "reduce_min",
1895                "backend does not accept borrowed tensor views at this execution boundary",
1896            )),
1897        }
1898    }
1899}
1900
1901/// Dot-general operations.
1902///
1903/// # Examples
1904///
1905/// ```rust
1906/// use tenferro_tensor::TensorDot;
1907///
1908/// fn accepts_dot<B: TensorDot>(_backend: &mut B) {}
1909/// ```
1910pub trait TensorDot: TensorElementwise {
1911    fn dot_general(
1912        &mut self,
1913        lhs: &Tensor,
1914        rhs: &Tensor,
1915        config: &DotGeneralConfig,
1916    ) -> crate::Result<Tensor>;
1917
1918    #[doc(hidden)]
1919    fn dot_general_read(
1920        &mut self,
1921        lhs: TensorRead<'_>,
1922        rhs: TensorRead<'_>,
1923        config: &DotGeneralConfig,
1924    ) -> crate::Result<Tensor> {
1925        match (lhs.as_tensor(), rhs.as_tensor()) {
1926            (Some(lhs), Some(rhs)) => self.dot_general(lhs, rhs, config),
1927            _ => {
1928                let lhs = lhs.to_tensor()?;
1929                let rhs = rhs.to_tensor()?;
1930                self.dot_general(&lhs, &rhs, config)
1931            }
1932        }
1933    }
1934
1935    /// Overwrite caller-provided output with dot-general from read inputs.
1936    ///
1937    /// This is the dot/GEMM spelling of `_into`: the previous output value is
1938    /// not read. Use [`TensorDot::dot_general_read_into_accum`] for explicit
1939    /// read-modify-write accumulation.
1940    ///
1941    /// # Examples
1942    ///
1943    /// ```rust
1944    /// use tenferro_tensor::{DotGeneralConfig, TensorDot, TensorRead, TensorWrite};
1945    ///
1946    /// fn dot_into<B: TensorDot>(
1947    ///     backend: &mut B,
1948    ///     lhs: TensorRead<'_>,
1949    ///     rhs: TensorRead<'_>,
1950    ///     config: &DotGeneralConfig,
1951    ///     out: TensorWrite<'_>,
1952    /// ) -> tenferro_tensor::Result<()> {
1953    ///     backend.dot_general_read_into(lhs, rhs, config, out)
1954    /// }
1955    /// ```
1956    fn dot_general_read_into(
1957        &mut self,
1958        lhs: TensorRead<'_>,
1959        rhs: TensorRead<'_>,
1960        config: &DotGeneralConfig,
1961        out: TensorWrite<'_>,
1962    ) -> crate::Result<()> {
1963        let accumulation = DotGeneralAccumulation::overwrite(lhs.dtype())?;
1964        self.dot_general_read_into_accum(lhs, rhs, config, accumulation, out)
1965    }
1966
1967    #[doc(hidden)]
1968    fn dot_general_with_conj(
1969        &mut self,
1970        lhs: &Tensor,
1971        rhs: &Tensor,
1972        config: &DotGeneralConfig,
1973        lhs_conj: bool,
1974        rhs_conj: bool,
1975    ) -> crate::Result<Tensor> {
1976        if !lhs_conj && !rhs_conj {
1977            return self.dot_general(lhs, rhs, config);
1978        }
1979
1980        let lhs_tmp;
1981        let lhs_ref = if lhs_conj {
1982            lhs_tmp = self.conj(lhs)?;
1983            &lhs_tmp
1984        } else {
1985            lhs
1986        };
1987        let rhs_tmp;
1988        let rhs_ref = if rhs_conj {
1989            rhs_tmp = self.conj(rhs)?;
1990            &rhs_tmp
1991        } else {
1992            rhs
1993        };
1994        self.dot_general(lhs_ref, rhs_ref, config)
1995    }
1996
1997    #[allow(clippy::too_many_arguments)]
1998    #[doc(hidden)]
1999    fn dot_general_with_conj_read(
2000        &mut self,
2001        lhs: TensorRead<'_>,
2002        rhs: TensorRead<'_>,
2003        config: &DotGeneralConfig,
2004        lhs_conj: bool,
2005        rhs_conj: bool,
2006    ) -> crate::Result<Tensor> {
2007        if !lhs_conj && !rhs_conj {
2008            return self.dot_general_read(lhs, rhs, config);
2009        }
2010
2011        let lhs_tmp;
2012        let lhs_ref = if let Some(tensor) = lhs.as_tensor() {
2013            tensor
2014        } else {
2015            lhs_tmp = lhs.to_tensor()?;
2016            &lhs_tmp
2017        };
2018        let rhs_tmp;
2019        let rhs_ref = if let Some(tensor) = rhs.as_tensor() {
2020            tensor
2021        } else {
2022            rhs_tmp = rhs.to_tensor()?;
2023            &rhs_tmp
2024        };
2025        self.dot_general_with_conj(lhs_ref, rhs_ref, config, lhs_conj, rhs_conj)
2026    }
2027
2028    /// Apply scaled dot-general accumulation into caller-provided output.
2029    ///
2030    /// This is explicitly read-modify-write when `accumulation.beta` is nonzero:
2031    /// `out = alpha * dot_general(lhs, rhs) + beta * out`.
2032    ///
2033    /// # Examples
2034    ///
2035    /// ```rust
2036    /// use tenferro_tensor::{
2037    ///     DotGeneralAccumulation, DotGeneralConfig, TensorDot, TensorRead, TensorWrite,
2038    /// };
2039    ///
2040    /// fn dot_add_to<B: TensorDot>(
2041    ///     backend: &mut B,
2042    ///     lhs: TensorRead<'_>,
2043    ///     rhs: TensorRead<'_>,
2044    ///     config: &DotGeneralConfig,
2045    ///     out: TensorWrite<'_>,
2046    /// ) -> tenferro_tensor::Result<()> {
2047    ///     let accumulation = DotGeneralAccumulation::add_to(lhs.dtype())?;
2048    ///     backend.dot_general_read_into_accum(lhs, rhs, config, accumulation, out)
2049    /// }
2050    /// ```
2051    fn dot_general_read_into_accum(
2052        &mut self,
2053        lhs: TensorRead<'_>,
2054        rhs: TensorRead<'_>,
2055        config: &DotGeneralConfig,
2056        accumulation: DotGeneralAccumulation,
2057        out: TensorWrite<'_>,
2058    ) -> crate::Result<()> {
2059        dot_general_accum_via_temp(self, lhs, rhs, config, accumulation, out)
2060    }
2061}
2062
2063/// Session-scoped cached dot-general operations.
2064///
2065/// # Examples
2066///
2067/// ```rust
2068/// use tenferro_tensor::BackendSession;
2069///
2070/// fn accepts_session_dot<S: BackendSession + ?Sized>(_session: &mut S) {}
2071/// ```
2072pub trait SessionCachedDot: TensorDot {
2073    #[doc(hidden)]
2074    fn dot_general_cached(
2075        &mut self,
2076        _cache_slot: Option<usize>,
2077        lhs: &Tensor,
2078        rhs: &Tensor,
2079        config: &DotGeneralConfig,
2080    ) -> crate::Result<Tensor> {
2081        self.dot_general(lhs, rhs, config)
2082    }
2083
2084    #[doc(hidden)]
2085    fn dot_general_read_cached(
2086        &mut self,
2087        cache_slot: Option<usize>,
2088        lhs: TensorRead<'_>,
2089        rhs: TensorRead<'_>,
2090        config: &DotGeneralConfig,
2091    ) -> crate::Result<Tensor> {
2092        match (lhs.as_tensor(), rhs.as_tensor()) {
2093            (Some(lhs), Some(rhs)) => self.dot_general_cached(cache_slot, lhs, rhs, config),
2094            _ => {
2095                let lhs = lhs.to_tensor()?;
2096                let rhs = rhs.to_tensor()?;
2097                self.dot_general_cached(cache_slot, &lhs, &rhs, config)
2098            }
2099        }
2100    }
2101
2102    // Mirrors the dot-general signature plus runtime-cache metadata.
2103    #[allow(clippy::too_many_arguments)]
2104    #[doc(hidden)]
2105    fn dot_general_with_conj_cached(
2106        &mut self,
2107        _cache_slot: Option<usize>,
2108        lhs: &Tensor,
2109        rhs: &Tensor,
2110        config: &DotGeneralConfig,
2111        lhs_conj: bool,
2112        rhs_conj: bool,
2113    ) -> crate::Result<Tensor> {
2114        self.dot_general_with_conj(lhs, rhs, config, lhs_conj, rhs_conj)
2115    }
2116
2117    // Mirrors the dot-general read signature plus runtime-cache metadata.
2118    #[allow(clippy::too_many_arguments)]
2119    #[doc(hidden)]
2120    fn dot_general_with_conj_read_cached(
2121        &mut self,
2122        cache_slot: Option<usize>,
2123        lhs: TensorRead<'_>,
2124        rhs: TensorRead<'_>,
2125        config: &DotGeneralConfig,
2126        lhs_conj: bool,
2127        rhs_conj: bool,
2128    ) -> crate::Result<Tensor> {
2129        if !lhs_conj && !rhs_conj {
2130            return self.dot_general_read_cached(cache_slot, lhs, rhs, config);
2131        }
2132
2133        let lhs_tmp;
2134        let lhs_ref = if let Some(tensor) = lhs.as_tensor() {
2135            tensor
2136        } else {
2137            lhs_tmp = lhs.to_tensor()?;
2138            &lhs_tmp
2139        };
2140        let rhs_tmp;
2141        let rhs_ref = if let Some(tensor) = rhs.as_tensor() {
2142            tensor
2143        } else {
2144            rhs_tmp = rhs.to_tensor()?;
2145            &rhs_tmp
2146        };
2147        self.dot_general_with_conj_cached(cache_slot, lhs_ref, rhs_ref, config, lhs_conj, rhs_conj)
2148    }
2149
2150    /// Apply session-cached scaled dot-general accumulation into output.
2151    ///
2152    /// The cache slot is session-local metadata; `accumulation` still controls
2153    /// overwrite versus read-modify-write semantics.
2154    ///
2155    /// # Examples
2156    ///
2157    /// ```rust
2158    /// use tenferro_tensor::{
2159    ///     DotGeneralAccumulation, DotGeneralConfig, SessionCachedDot, TensorRead, TensorWrite,
2160    /// };
2161    ///
2162    /// fn session_cached_dot_add_to<S: SessionCachedDot + ?Sized>(
2163    ///     session: &mut S,
2164    ///     lhs: TensorRead<'_>,
2165    ///     rhs: TensorRead<'_>,
2166    ///     config: &DotGeneralConfig,
2167    ///     out: TensorWrite<'_>,
2168    /// ) -> tenferro_tensor::Result<()> {
2169    ///     let accumulation = DotGeneralAccumulation::add_to(lhs.dtype())?;
2170    ///     session.dot_general_read_into_accum_cached(
2171    ///         Some(0),
2172    ///         lhs,
2173    ///         rhs,
2174    ///         config,
2175    ///         accumulation,
2176    ///         out,
2177    ///     )
2178    /// }
2179    /// ```
2180    fn dot_general_read_into_accum_cached(
2181        &mut self,
2182        _cache_slot: Option<usize>,
2183        lhs: TensorRead<'_>,
2184        rhs: TensorRead<'_>,
2185        config: &DotGeneralConfig,
2186        accumulation: DotGeneralAccumulation,
2187        out: TensorWrite<'_>,
2188    ) -> crate::Result<()> {
2189        self.dot_general_read_into_accum(lhs, rhs, config, accumulation, out)
2190    }
2191
2192    #[doc(hidden)]
2193    fn grouped_gemm_cached(
2194        &mut self,
2195        _cache_slot: Option<usize>,
2196        lhs: TensorRead<'_>,
2197        rhs: TensorRead<'_>,
2198        config: &GroupedGemmConfig<'_>,
2199        out: TensorWrite<'_>,
2200    ) -> crate::Result<()> {
2201        grouped_gemm_default(self, lhs, rhs, config, out)
2202    }
2203}
2204
2205/// Indexing, slicing, and padding operations.
2206///
2207/// # Examples
2208///
2209/// ```rust
2210/// use tenferro_tensor::TensorIndexing;
2211///
2212/// fn accepts_indexing<B: TensorIndexing>(_backend: &mut B) {}
2213/// ```
2214pub trait TensorIndexing {
2215    fn gather(
2216        &mut self,
2217        operand: &Tensor,
2218        start_indices: &Tensor,
2219        config: &GatherConfig,
2220    ) -> crate::Result<Tensor>;
2221    fn scatter(
2222        &mut self,
2223        operand: &Tensor,
2224        scatter_indices: &Tensor,
2225        updates: &Tensor,
2226        config: &ScatterConfig,
2227    ) -> crate::Result<Tensor>;
2228    fn slice(&mut self, input: &Tensor, config: &SliceConfig) -> crate::Result<Tensor>;
2229    fn dynamic_slice(
2230        &mut self,
2231        input: &Tensor,
2232        starts: &Tensor,
2233        slice_sizes: &[usize],
2234    ) -> crate::Result<Tensor>;
2235    fn dynamic_update_slice(
2236        &mut self,
2237        operand: &Tensor,
2238        update: &Tensor,
2239        starts: &Tensor,
2240    ) -> crate::Result<Tensor>;
2241    fn pad(&mut self, input: &Tensor, config: &PadConfig) -> crate::Result<Tensor>;
2242    fn concatenate(&mut self, inputs: &[&Tensor], axis: usize) -> crate::Result<Tensor>;
2243    fn reverse(&mut self, input: &Tensor, axes: &[usize]) -> crate::Result<Tensor>;
2244}
2245
2246/// Backend-owned canonicalization for typed tensor views.
2247///
2248/// Implementations must preserve the input placement family. CPU backends
2249/// canonicalize host views through explicit host copies and reject backend
2250/// buffers with a diagnostic that asks the caller to download first. GPU
2251/// backends canonicalize GPU-resident views on the same device and reject host
2252/// buffers with an upload hint.
2253///
2254/// This trait is intentionally separate from [`BackendSession`] so generic
2255/// typed methods do not change the object-safety contract of `dyn BackendSession`.
2256///
2257/// # Examples
2258///
2259/// ```rust
2260/// use tenferro_tensor::{DynRank, TensorViewCanonicalization, TypedTensor};
2261///
2262/// fn compact_i32<B: TensorViewCanonicalization<i32, DynRank>>(
2263///     backend: &mut B,
2264///     tensor: &TypedTensor<i32>,
2265/// ) -> tenferro_tensor::Result<TypedTensor<i32>> {
2266///     backend.to_contiguous(&tensor.as_view())
2267/// }
2268/// ```
2269pub trait TensorViewCanonicalization<T: Clone + 'static, R: TensorRank> {
2270    fn to_contiguous(
2271        &mut self,
2272        view: &TypedTensorView<'_, T, R>,
2273    ) -> crate::Result<TypedTensor<T, R>>;
2274
2275    fn copy_from_contiguous(
2276        &mut self,
2277        src: &TypedTensor<T, R>,
2278        dst: &mut TypedTensorViewMut<'_, T, R>,
2279    ) -> crate::Result<()>;
2280}
2281
2282/// Optional elementwise fusion execution.
2283///
2284/// # Examples
2285///
2286/// ```rust
2287/// use tenferro_tensor::TensorFusion;
2288///
2289/// fn accepts_fusion<B: TensorFusion>(_backend: &mut B) {}
2290/// ```
2291pub trait TensorFusion {
2292    #[doc(hidden)]
2293    fn execute_elementwise_fusion(
2294        &mut self,
2295        _inputs: &[&Tensor],
2296        _plan: &ElementwiseFusionPlan,
2297    ) -> crate::Result<Option<Vec<Tensor>>> {
2298        Ok(None)
2299    }
2300
2301    #[doc(hidden)]
2302    #[allow(clippy::too_many_arguments)]
2303    fn execute_broadcast_multiply(
2304        &mut self,
2305        _lhs: TensorRead<'_>,
2306        _lhs_shape: &[usize],
2307        _lhs_dims: &[usize],
2308        _rhs: TensorRead<'_>,
2309        _rhs_shape: &[usize],
2310        _rhs_dims: &[usize],
2311    ) -> crate::Result<Option<Tensor>> {
2312        Ok(None)
2313    }
2314
2315    #[doc(hidden)]
2316    #[allow(clippy::too_many_arguments)]
2317    fn execute_broadcast_multiply_value(
2318        &mut self,
2319        lhs: TensorRead<'_>,
2320        lhs_shape: &[usize],
2321        lhs_dims: &[usize],
2322        rhs: TensorRead<'_>,
2323        rhs_shape: &[usize],
2324        rhs_dims: &[usize],
2325    ) -> crate::Result<Option<TensorValue>> {
2326        self.execute_broadcast_multiply(lhs, lhs_shape, lhs_dims, rhs, rhs_shape, rhs_dims)
2327            .map(|tensor| tensor.map(TensorValue::from_tensor))
2328    }
2329}
2330
2331/// Backend buffer lifecycle operations.
2332///
2333/// # Examples
2334///
2335/// ```rust
2336/// use tenferro_tensor::TensorBuffer;
2337///
2338/// fn accepts_buffer<B: TensorBuffer>(_backend: &mut B) {}
2339/// ```
2340pub trait TensorBuffer {
2341    fn reclaim_buffer(&mut self, _tensor: Tensor) {}
2342}
2343
2344/// Device transfer operations on backend boundaries.
2345///
2346/// # Examples
2347///
2348/// ```rust
2349/// use tenferro_tensor::TensorDeviceTransfer;
2350///
2351/// fn accepts_transfer<B: TensorDeviceTransfer>(_backend: &mut B) {}
2352/// ```
2353pub trait TensorDeviceTransfer {
2354    fn download_to_host(&mut self, tensor: &Tensor) -> crate::Result<Tensor> {
2355        Ok(tensor.clone())
2356    }
2357
2358    fn upload_host_tensor(&mut self, tensor: &Tensor) -> crate::Result<Tensor> {
2359        Ok(tensor.clone())
2360    }
2361}
2362
2363/// Runtime cache associated with a backend.
2364///
2365/// # Examples
2366///
2367/// ```rust
2368/// use tenferro_tensor::BackendRuntimeCache;
2369///
2370/// fn accepts_runtime_cache<B: BackendRuntimeCache>(_backend: &B) {}
2371/// ```
2372pub trait BackendRuntimeCache {
2373    #[doc(hidden)]
2374    type RuntimeCache: RuntimeCacheControl + Send + Sync + 'static;
2375}
2376
2377/// Backend-owned cached dot-general operations.
2378///
2379/// # Examples
2380///
2381/// ```rust
2382/// use tenferro_tensor::BackendCachedDot;
2383///
2384/// fn accepts_backend_cached_dot<B: BackendCachedDot>(_backend: &mut B) {}
2385/// ```
2386pub trait BackendCachedDot: BackendRuntimeCache + TensorDot {
2387    #[doc(hidden)]
2388    fn dot_general_cached(
2389        &mut self,
2390        _cache: &mut Self::RuntimeCache,
2391        _cache_slot: Option<usize>,
2392        lhs: &Tensor,
2393        rhs: &Tensor,
2394        config: &DotGeneralConfig,
2395    ) -> crate::Result<Tensor> {
2396        self.dot_general(lhs, rhs, config)
2397    }
2398
2399    #[doc(hidden)]
2400    fn dot_general_read_cached(
2401        &mut self,
2402        cache: &mut Self::RuntimeCache,
2403        cache_slot: Option<usize>,
2404        lhs: TensorRead<'_>,
2405        rhs: TensorRead<'_>,
2406        config: &DotGeneralConfig,
2407    ) -> crate::Result<Tensor> {
2408        match (lhs.as_tensor(), rhs.as_tensor()) {
2409            (Some(lhs), Some(rhs)) => self.dot_general_cached(cache, cache_slot, lhs, rhs, config),
2410            _ => {
2411                let lhs = lhs.to_tensor()?;
2412                let rhs = rhs.to_tensor()?;
2413                self.dot_general_cached(cache, cache_slot, &lhs, &rhs, config)
2414            }
2415        }
2416    }
2417
2418    // Mirrors the dot-general signature plus runtime-cache metadata.
2419    #[allow(clippy::too_many_arguments)]
2420    #[doc(hidden)]
2421    fn dot_general_with_conj_cached(
2422        &mut self,
2423        _cache: &mut Self::RuntimeCache,
2424        _cache_slot: Option<usize>,
2425        lhs: &Tensor,
2426        rhs: &Tensor,
2427        config: &DotGeneralConfig,
2428        lhs_conj: bool,
2429        rhs_conj: bool,
2430    ) -> crate::Result<Tensor> {
2431        self.dot_general_with_conj(lhs, rhs, config, lhs_conj, rhs_conj)
2432    }
2433
2434    // Mirrors the dot-general read signature plus runtime-cache metadata.
2435    #[allow(clippy::too_many_arguments)]
2436    #[doc(hidden)]
2437    fn dot_general_with_conj_read_cached(
2438        &mut self,
2439        cache: &mut Self::RuntimeCache,
2440        cache_slot: Option<usize>,
2441        lhs: TensorRead<'_>,
2442        rhs: TensorRead<'_>,
2443        config: &DotGeneralConfig,
2444        lhs_conj: bool,
2445        rhs_conj: bool,
2446    ) -> crate::Result<Tensor> {
2447        if !lhs_conj && !rhs_conj {
2448            return self.dot_general_read_cached(cache, cache_slot, lhs, rhs, config);
2449        }
2450
2451        let lhs_tmp;
2452        let lhs_ref = if let Some(tensor) = lhs.as_tensor() {
2453            tensor
2454        } else {
2455            lhs_tmp = lhs.to_tensor()?;
2456            &lhs_tmp
2457        };
2458        let rhs_tmp;
2459        let rhs_ref = if let Some(tensor) = rhs.as_tensor() {
2460            tensor
2461        } else {
2462            rhs_tmp = rhs.to_tensor()?;
2463            &rhs_tmp
2464        };
2465        self.dot_general_with_conj_cached(
2466            cache, cache_slot, lhs_ref, rhs_ref, config, lhs_conj, rhs_conj,
2467        )
2468    }
2469
2470    /// Apply cached scaled dot-general accumulation into caller-provided output.
2471    ///
2472    /// The cache slot identifies backend-local analysis metadata only; output
2473    /// semantics are still fully described by `accumulation`.
2474    ///
2475    /// # Examples
2476    ///
2477    /// ```rust
2478    /// use tenferro_tensor::{
2479    ///     BackendCachedDot, BackendRuntimeCache, DotGeneralAccumulation, DotGeneralConfig,
2480    ///     TensorRead, TensorWrite,
2481    /// };
2482    ///
2483    /// fn cached_dot_add_to<B: BackendCachedDot>(
2484    ///     backend: &mut B,
2485    ///     cache: &mut B::RuntimeCache,
2486    ///     lhs: TensorRead<'_>,
2487    ///     rhs: TensorRead<'_>,
2488    ///     config: &DotGeneralConfig,
2489    ///     out: TensorWrite<'_>,
2490    /// ) -> tenferro_tensor::Result<()>
2491    /// where
2492    ///     B: BackendRuntimeCache,
2493    /// {
2494    ///     let accumulation = DotGeneralAccumulation::add_to(lhs.dtype())?;
2495    ///     backend.dot_general_read_into_accum_cached(
2496    ///         cache,
2497    ///         Some(0),
2498    ///         lhs,
2499    ///         rhs,
2500    ///         config,
2501    ///         accumulation,
2502    ///         out,
2503    ///     )
2504    /// }
2505    /// ```
2506    #[allow(clippy::too_many_arguments)]
2507    fn dot_general_read_into_accum_cached(
2508        &mut self,
2509        _cache: &mut Self::RuntimeCache,
2510        _cache_slot: Option<usize>,
2511        lhs: TensorRead<'_>,
2512        rhs: TensorRead<'_>,
2513        config: &DotGeneralConfig,
2514        accumulation: DotGeneralAccumulation,
2515        out: TensorWrite<'_>,
2516    ) -> crate::Result<()> {
2517        self.dot_general_read_into_accum(lhs, rhs, config, accumulation, out)
2518    }
2519
2520    #[doc(hidden)]
2521    fn grouped_gemm_cached(
2522        &mut self,
2523        _cache: &mut Self::RuntimeCache,
2524        _cache_slot: Option<usize>,
2525        lhs: TensorRead<'_>,
2526        rhs: TensorRead<'_>,
2527        config: &GroupedGemmConfig<'_>,
2528        out: TensorWrite<'_>,
2529    ) -> crate::Result<()> {
2530        grouped_gemm_default(self, lhs, rhs, config, out)
2531    }
2532}
2533
2534/// Backend execution-session entry points.
2535///
2536/// # Examples
2537///
2538/// ```rust
2539/// use tenferro_tensor::BackendSessionHost;
2540///
2541/// fn accepts_session_host<B: BackendSessionHost>(_backend: &mut B) {}
2542/// ```
2543pub trait BackendSessionHost: BackendRuntimeCache {
2544    fn with_backend_session<R: Send>(
2545        &mut self,
2546        f: impl FnOnce(&mut dyn BackendSession) -> R + Send,
2547    ) -> R
2548    where
2549        Self: TensorBackend + Sized,
2550    {
2551        default_backend_session(self, f)
2552    }
2553
2554    #[doc(hidden)]
2555    fn with_backend_session_cached<R: Send>(
2556        &mut self,
2557        _cache: &mut Self::RuntimeCache,
2558        f: impl FnOnce(&mut dyn BackendSession) -> R + Send,
2559    ) -> R
2560    where
2561        Self: TensorBackend + Sized,
2562    {
2563        self.with_backend_session(f)
2564    }
2565}
2566
2567/// Operation capabilities shared by backends and backend sessions.
2568#[doc(hidden)]
2569pub trait TensorBackendOps:
2570    TensorElementwise
2571    + TensorAnalytic
2572    + TensorStructural
2573    + TensorReduction
2574    + TensorIndexing
2575    + TensorDot
2576    + TensorFusion
2577    + TensorBuffer
2578{
2579}
2580
2581impl<T> TensorBackendOps for T where
2582    T: TensorElementwise
2583        + TensorAnalytic
2584        + TensorStructural
2585        + TensorReduction
2586        + TensorIndexing
2587        + TensorDot
2588        + TensorFusion
2589        + TensorBuffer
2590        + ?Sized
2591{
2592}
2593
2594/// Execution session surface for dense tensor backends.
2595///
2596/// All operations run within a backend-owned execution scope such as a CPU
2597/// thread policy or a GPU stream. Individual ops must not try to re-enter that
2598/// scope.
2599///
2600/// # Examples
2601///
2602/// ```rust
2603/// use tenferro_tensor::{BackendSessionHost, Tensor, TypedTensor};
2604///
2605/// fn add_in_session<B: BackendSessionHost>(
2606///     backend: &mut B,
2607///     a: &Tensor,
2608///     b: &Tensor,
2609/// ) -> tenferro_tensor::Result<Tensor>
2610/// where
2611///     B: tenferro_tensor::TensorBackend,
2612/// {
2613///     backend.with_backend_session(|exec| exec.add(a, b))
2614/// }
2615/// ```
2616pub trait BackendSession: TensorBackendOps + SessionCachedDot {}
2617
2618impl<T> BackendSession for T where T: TensorBackendOps + SessionCachedDot + ?Sized {}
2619
2620/// Standard runtime backend over dynamic [`Tensor`] values.
2621///
2622/// # Examples
2623///
2624/// ```rust
2625/// use tenferro_tensor::TensorBackend;
2626///
2627/// fn accepts_backend<B: TensorBackend>(_backend: &mut B) {}
2628/// ```
2629pub trait TensorBackend:
2630    BackendRuntimeCache
2631    + TensorBackendOps
2632    + BackendCachedDot
2633    + TensorDeviceTransfer
2634    + BackendSessionHost
2635{
2636}
2637
2638impl<T> SessionCachedDot for T where T: TensorBackend + ?Sized {}
2639
2640/// Run a closure using the backend itself as a default execution session.
2641///
2642/// This is suitable for backends whose individual ops already manage their own
2643/// execution context.
2644///
2645/// # Examples
2646///
2647/// ```rust
2648/// use tenferro_tensor::{default_backend_session, TensorBackend};
2649///
2650/// fn run_with_default_session<B: TensorBackend>(backend: &mut B) -> usize {
2651///     default_backend_session(backend, |_exec| 1usize)
2652/// }
2653/// ```
2654pub fn default_backend_session<B: TensorBackend, R: Send>(
2655    backend: &mut B,
2656    f: impl FnOnce(&mut dyn BackendSession) -> R + Send,
2657) -> R {
2658    f(backend)
2659}