Skip to main content

tenferro_runtime/
typed_tensor.rs

1//! Typed tensor operation extension traits.
2//!
3//! Operation families that are no longer part of core, including einsum, live
4//! in their extension crates.
5
6use tenferro_ops::broadcast::{broadcast_input_plan, broadcast_shape, broadcast_shapes};
7use tenferro_tensor::validate::matmul_config_for_shapes;
8use tenferro_tensor::{CompareDir, Error, Result, Tensor, TensorBackend, TensorRead, TensorScalar};
9
10use crate::{TypedTensorMaskOpsExt, TypedTensorOpsExt};
11use tenferro_tensor::TypedTensor;
12
13impl<T: TensorScalar> TypedTensorOpsExt<T> for TypedTensor<T> {
14    fn add<B: TensorBackend>(
15        &self,
16        rhs: &TypedTensor<T>,
17        backend: &mut B,
18    ) -> Result<TypedTensor<T>> {
19        add(self, rhs, backend)
20    }
21
22    fn sub<B: TensorBackend>(
23        &self,
24        rhs: &TypedTensor<T>,
25        backend: &mut B,
26    ) -> Result<TypedTensor<T>> {
27        sub(self, rhs, backend)
28    }
29
30    fn mul<B: TensorBackend>(
31        &self,
32        rhs: &TypedTensor<T>,
33        backend: &mut B,
34    ) -> Result<TypedTensor<T>> {
35        mul(self, rhs, backend)
36    }
37
38    fn div<B: TensorBackend>(
39        &self,
40        rhs: &TypedTensor<T>,
41        backend: &mut B,
42    ) -> Result<TypedTensor<T>> {
43        div(self, rhs, backend)
44    }
45
46    fn rem<B: TensorBackend>(
47        &self,
48        rhs: &TypedTensor<T>,
49        backend: &mut B,
50    ) -> Result<TypedTensor<T>> {
51        rem(self, rhs, backend)
52    }
53
54    fn pow<B: TensorBackend>(
55        &self,
56        rhs: &TypedTensor<T>,
57        backend: &mut B,
58    ) -> Result<TypedTensor<T>> {
59        pow(self, rhs, backend)
60    }
61
62    fn maximum<B: TensorBackend>(
63        &self,
64        rhs: &TypedTensor<T>,
65        backend: &mut B,
66    ) -> Result<TypedTensor<T>> {
67        maximum(self, rhs, backend)
68    }
69
70    fn minimum<B: TensorBackend>(
71        &self,
72        rhs: &TypedTensor<T>,
73        backend: &mut B,
74    ) -> Result<TypedTensor<T>> {
75        minimum(self, rhs, backend)
76    }
77
78    fn neg<B: TensorBackend>(&self, backend: &mut B) -> Result<TypedTensor<T>> {
79        neg(self, backend)
80    }
81
82    fn abs<B: TensorBackend>(&self, backend: &mut B) -> Result<TypedTensor<T>> {
83        abs(self, backend)
84    }
85
86    fn sign<B: TensorBackend>(&self, backend: &mut B) -> Result<TypedTensor<T>> {
87        sign(self, backend)
88    }
89
90    fn conj<B: TensorBackend>(&self, backend: &mut B) -> Result<TypedTensor<T>> {
91        conj(self, backend)
92    }
93
94    fn exp<B: TensorBackend>(&self, backend: &mut B) -> Result<TypedTensor<T>> {
95        exp(self, backend)
96    }
97
98    fn log<B: TensorBackend>(&self, backend: &mut B) -> Result<TypedTensor<T>> {
99        log(self, backend)
100    }
101
102    fn sin<B: TensorBackend>(&self, backend: &mut B) -> Result<TypedTensor<T>> {
103        sin(self, backend)
104    }
105
106    fn cos<B: TensorBackend>(&self, backend: &mut B) -> Result<TypedTensor<T>> {
107        cos(self, backend)
108    }
109
110    fn tanh<B: TensorBackend>(&self, backend: &mut B) -> Result<TypedTensor<T>> {
111        tanh(self, backend)
112    }
113
114    fn sqrt<B: TensorBackend>(&self, backend: &mut B) -> Result<TypedTensor<T>> {
115        sqrt(self, backend)
116    }
117
118    fn rsqrt<B: TensorBackend>(&self, backend: &mut B) -> Result<TypedTensor<T>> {
119        rsqrt(self, backend)
120    }
121
122    fn expm1<B: TensorBackend>(&self, backend: &mut B) -> Result<TypedTensor<T>> {
123        expm1(self, backend)
124    }
125
126    fn log1p<B: TensorBackend>(&self, backend: &mut B) -> Result<TypedTensor<T>> {
127        log1p(self, backend)
128    }
129
130    fn compare<B: TensorBackend>(
131        &self,
132        rhs: &TypedTensor<T>,
133        dir: CompareDir,
134        backend: &mut B,
135    ) -> Result<TypedTensor<bool>> {
136        compare(self, rhs, dir, backend)
137    }
138
139    fn clamp<B: TensorBackend>(
140        &self,
141        lower: &TypedTensor<T>,
142        upper: &TypedTensor<T>,
143        backend: &mut B,
144    ) -> Result<TypedTensor<T>> {
145        clamp(self, lower, upper, backend)
146    }
147
148    fn matmul<B: TensorBackend>(
149        &self,
150        rhs: &TypedTensor<T>,
151        backend: &mut B,
152    ) -> Result<TypedTensor<T>> {
153        matmul(self, rhs, backend)
154    }
155
156    fn reduce_sum<B: TensorBackend>(
157        &self,
158        axes: &[usize],
159        backend: &mut B,
160    ) -> Result<TypedTensor<T>> {
161        reduce_sum(self, axes, backend)
162    }
163
164    fn reshape<B: TensorBackend>(
165        &self,
166        shape: &[usize],
167        backend: &mut B,
168    ) -> Result<TypedTensor<T>> {
169        reshape(self, shape, backend)
170    }
171
172    fn transpose<B: TensorBackend>(
173        &self,
174        perm: &[usize],
175        backend: &mut B,
176    ) -> Result<TypedTensor<T>> {
177        transpose(self, perm, backend)
178    }
179
180    fn broadcast_in_dim<B: TensorBackend>(
181        &self,
182        shape: &[usize],
183        dims: &[usize],
184        backend: &mut B,
185    ) -> Result<TypedTensor<T>> {
186        broadcast_in_dim(self, shape, dims, backend)
187    }
188}
189
190impl TypedTensorMaskOpsExt for TypedTensor<bool> {
191    fn where_select<T: TensorScalar, B: TensorBackend>(
192        &self,
193        on_true: &TypedTensor<T>,
194        on_false: &TypedTensor<T>,
195        backend: &mut B,
196    ) -> Result<TypedTensor<T>> {
197        where_select(self, on_true, on_false, backend)
198    }
199}
200
201/// Elementwise addition with NumPy-style broadcasting.
202///
203/// # Examples
204///
205/// ```rust
206/// # use tenferro_cpu::CpuBackend;
207/// use tenferro_runtime::{TypedTensor, TypedTensorOpsExt};
208/// # let mut backend = CpuBackend::new();
209/// # let x = TypedTensor::<f64>::from_vec_col_major(vec![2], vec![1.0, 2.0]).unwrap();
210/// # let y = TypedTensor::<f64>::from_vec_col_major(vec![2], vec![3.0, 4.0]).unwrap();
211/// let z = x.add(&y, &mut backend).unwrap();
212/// ```
213fn add<T: TensorScalar>(
214    lhs: &TypedTensor<T>,
215    rhs: &TypedTensor<T>,
216    backend: &mut impl TensorBackend,
217) -> Result<TypedTensor<T>> {
218    let (lhs, rhs) = broadcast_binary_read(lhs, rhs, backend)?;
219    let out =
220        backend.with_backend_session(|exec| exec.add_read(lhs.tensor_read(), rhs.tensor_read()))?;
221    into_typed_result("add", out)
222}
223
224macro_rules! unary_fn {
225    ($name:ident, $method:ident, $summary:literal) => {
226        #[doc = $summary]
227        ///
228        /// # Examples
229        ///
230        /// ```rust
231        /// # use tenferro_cpu::CpuBackend;
232        /// use tenferro_runtime::{TypedTensor, TypedTensorOpsExt};
233        /// # let mut backend = CpuBackend::new();
234        /// # let x = TypedTensor::<f64>::from_vec_col_major(vec![2], vec![1.0, 4.0]).unwrap();
235        #[doc = concat!("let y = x.", stringify!($name), "(&mut backend).unwrap();")]
236        /// ```
237        fn $name<T: TensorScalar>(
238            input: &TypedTensor<T>,
239            backend: &mut impl TensorBackend,
240        ) -> Result<TypedTensor<T>> {
241            let out = backend.with_backend_session(|exec| exec.$method(T::tensor_read(input)))?;
242            into_typed_result(stringify!($name), out)
243        }
244    };
245}
246
247macro_rules! binary_fn {
248    ($name:ident, $method:ident, $summary:literal) => {
249        #[doc = $summary]
250        ///
251        /// # Examples
252        ///
253        /// ```rust
254        /// # use tenferro_cpu::CpuBackend;
255        /// use tenferro_runtime::{TypedTensor, TypedTensorOpsExt};
256        /// # let mut backend = CpuBackend::new();
257        /// # let x = TypedTensor::<f64>::from_vec_col_major(vec![2], vec![2.0, 4.0]).unwrap();
258        /// # let y = TypedTensor::<f64>::from_vec_col_major(vec![2], vec![1.0, 8.0]).unwrap();
259        #[doc = concat!("let z = x.", stringify!($name), "(&y, &mut backend).unwrap();")]
260        /// ```
261        fn $name<T: TensorScalar>(
262            lhs: &TypedTensor<T>,
263            rhs: &TypedTensor<T>,
264            backend: &mut impl TensorBackend,
265        ) -> Result<TypedTensor<T>> {
266            let (lhs, rhs) = broadcast_binary_read(lhs, rhs, backend)?;
267            let out = backend
268                .with_backend_session(|exec| exec.$method(lhs.tensor_read(), rhs.tensor_read()))?;
269            into_typed_result(stringify!($name), out)
270        }
271    };
272}
273
274binary_fn!(
275    mul,
276    mul_read,
277    "Elementwise multiplication with NumPy-style broadcasting."
278);
279binary_fn!(
280    div,
281    div_read,
282    "Elementwise division with NumPy-style broadcasting."
283);
284binary_fn!(
285    rem,
286    rem_read,
287    "Elementwise remainder with NumPy-style broadcasting."
288);
289binary_fn!(
290    pow,
291    pow_read,
292    "Elementwise power with NumPy-style broadcasting."
293);
294binary_fn!(
295    maximum,
296    maximum_read,
297    "Elementwise maximum with NumPy-style broadcasting."
298);
299binary_fn!(
300    minimum,
301    minimum_read,
302    "Elementwise minimum with NumPy-style broadcasting."
303);
304
305unary_fn!(neg, neg_read, "Elementwise negation.");
306unary_fn!(abs, abs_read, "Elementwise absolute value.");
307unary_fn!(sign, sign_read, "Elementwise sign.");
308unary_fn!(conj, conj_read, "Elementwise complex conjugate.");
309unary_fn!(exp, exp_read, "Elementwise exponential.");
310unary_fn!(log, log_read, "Elementwise natural logarithm.");
311unary_fn!(sin, sin_read, "Elementwise sine.");
312unary_fn!(cos, cos_read, "Elementwise cosine.");
313unary_fn!(tanh, tanh_read, "Elementwise hyperbolic tangent.");
314unary_fn!(sqrt, sqrt_read, "Elementwise square root.");
315unary_fn!(rsqrt, rsqrt_read, "Elementwise reciprocal square root.");
316unary_fn!(expm1, expm1_read, "Elementwise `exp(x) - 1`.");
317unary_fn!(log1p, log1p_read, "Elementwise `log(1 + x)`.");
318
319/// Elementwise subtraction with NumPy-style broadcasting.
320///
321/// # Examples
322///
323/// ```rust
324/// # use tenferro_cpu::CpuBackend;
325/// use tenferro_runtime::{TypedTensor, TypedTensorOpsExt};
326/// # let mut backend = CpuBackend::new();
327/// # let x = TypedTensor::<f64>::from_vec_col_major(vec![2], vec![2.0, 4.0]).unwrap();
328/// # let y = TypedTensor::<f64>::from_vec_col_major(vec![2], vec![1.0, 8.0]).unwrap();
329/// let z = x.sub(&y, &mut backend).unwrap();
330/// ```
331fn sub<T: TensorScalar>(
332    lhs: &TypedTensor<T>,
333    rhs: &TypedTensor<T>,
334    backend: &mut impl TensorBackend,
335) -> Result<TypedTensor<T>> {
336    let (lhs, rhs) = broadcast_binary_read(lhs, rhs, backend)?;
337    let out =
338        backend.with_backend_session(|exec| exec.sub_read(lhs.tensor_read(), rhs.tensor_read()))?;
339    into_typed_result("sub", out)
340}
341
342/// Elementwise comparison with NumPy-style broadcasting.
343///
344/// The result is a bool tensor.
345///
346/// # Examples
347///
348/// ```rust
349/// # use tenferro_cpu::CpuBackend;
350/// use tenferro_runtime::{CompareDir, TypedTensor, TypedTensorMaskOpsExt, TypedTensorOpsExt};
351/// # let mut backend = CpuBackend::new();
352/// # let x = TypedTensor::<f64>::from_vec_col_major(vec![2], vec![2.0, 4.0]).unwrap();
353/// # let y = TypedTensor::<f64>::from_vec_col_major(vec![2], vec![1.0, 8.0]).unwrap();
354/// let z = x.compare(&y, CompareDir::Gt, &mut backend).unwrap();
355/// assert_eq!(z.host_data().unwrap(), &[true, false]);
356/// ```
357fn compare<T: TensorScalar>(
358    lhs: &TypedTensor<T>,
359    rhs: &TypedTensor<T>,
360    dir: CompareDir,
361    backend: &mut impl TensorBackend,
362) -> Result<TypedTensor<bool>> {
363    let (lhs, rhs) = broadcast_binary_read(lhs, rhs, backend)?;
364    let out = backend.with_backend_session(|exec| {
365        exec.compare_read(lhs.tensor_read(), rhs.tensor_read(), &dir)
366    })?;
367    into_typed_result("compare", out)
368}
369
370/// Select values from `on_true` or `on_false` using a condition tensor.
371///
372/// This corresponds to NumPy `where(condition, x, y)`.
373///
374/// # Examples
375///
376/// ```rust
377/// # use tenferro_cpu::CpuBackend;
378/// use tenferro_runtime::{CompareDir, TypedTensor, TypedTensorMaskOpsExt, TypedTensorOpsExt};
379/// # let mut backend = CpuBackend::new();
380/// # let x = TypedTensor::<f64>::from_vec_col_major(vec![2], vec![2.0, 4.0]).unwrap();
381/// # let y = TypedTensor::<f64>::from_vec_col_major(vec![2], vec![1.0, 8.0]).unwrap();
382/// # let condition = x.compare(&y, CompareDir::Gt, &mut backend).unwrap();
383/// let z = condition.where_select(&x, &y, &mut backend).unwrap();
384/// ```
385fn where_select<T: TensorScalar>(
386    condition: &TypedTensor<bool>,
387    on_true: &TypedTensor<T>,
388    on_false: &TypedTensor<T>,
389    backend: &mut impl TensorBackend,
390) -> Result<TypedTensor<T>> {
391    let (condition, on_true, on_false) =
392        broadcast_ternary_read(condition, on_true, on_false, backend)?;
393    let out = backend.with_backend_session(|exec| {
394        exec.select_read(
395            condition.tensor_read(),
396            on_true.tensor_read(),
397            on_false.tensor_read(),
398        )
399    })?;
400    into_typed_result("where_select", out)
401}
402
403/// Clamp values elementwise between lower and upper bounds.
404///
405/// # Examples
406///
407/// ```rust
408/// # use tenferro_cpu::CpuBackend;
409/// use tenferro_runtime::{TypedTensor, TypedTensorOpsExt};
410/// # let mut backend = CpuBackend::new();
411/// # let x = TypedTensor::<f64>::from_vec_col_major(vec![2], vec![-2.0, 4.0]).unwrap();
412/// # let lower = TypedTensor::<f64>::from_vec_col_major(vec![], vec![0.0]).unwrap();
413/// # let upper = TypedTensor::<f64>::from_vec_col_major(vec![], vec![3.0]).unwrap();
414/// let z = x.clamp(&lower, &upper, &mut backend).unwrap();
415/// ```
416fn clamp<T: TensorScalar>(
417    input: &TypedTensor<T>,
418    lower: &TypedTensor<T>,
419    upper: &TypedTensor<T>,
420    backend: &mut impl TensorBackend,
421) -> Result<TypedTensor<T>> {
422    let (input, lower, upper) = broadcast_ternary_read(input, lower, upper, backend)?;
423    let out = backend.with_backend_session(|exec| {
424        exec.clamp_read(
425            input.tensor_read(),
426            lower.tensor_read(),
427            upper.tensor_read(),
428        )
429    })?;
430    into_typed_result("clamp", out)
431}
432
433/// Matrix multiplication helper for rank-2 typed tensors.
434///
435/// This contracts the last dimension of `a` with the first dimension of `b`.
436///
437/// # Examples
438///
439/// ```rust
440/// # use tenferro_cpu::CpuBackend;
441/// use tenferro_runtime::{TypedTensor, TypedTensorOpsExt};
442/// # let mut backend = CpuBackend::new();
443/// # let a = TypedTensor::<f64>::from_vec_col_major(vec![2, 3], vec![1.0; 6]).unwrap();
444/// # let b = TypedTensor::<f64>::from_vec_col_major(vec![3, 2], vec![1.0; 6]).unwrap();
445/// let c = a.matmul(&b, &mut backend).unwrap();
446/// ```
447fn matmul<T: TensorScalar>(
448    a: &TypedTensor<T>,
449    b: &TypedTensor<T>,
450    backend: &mut impl TensorBackend,
451) -> Result<TypedTensor<T>> {
452    let config = matmul_config_for_shapes("matmul", a.shape(), b.shape())?;
453    let out = backend.with_backend_session(|exec| {
454        exec.dot_general_read(T::tensor_read(a), T::tensor_read(b), &config)
455    })?;
456    into_typed_result("matmul", out)
457}
458
459/// Sum elements across one or more axes.
460///
461/// # Examples
462///
463/// ```rust
464/// # use tenferro_cpu::CpuBackend;
465/// use tenferro_runtime::{TypedTensor, TypedTensorOpsExt};
466/// # let mut backend = CpuBackend::new();
467/// let x = TypedTensor::<f64>::from_vec_col_major(
468///     vec![2, 3],
469///     vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0],
470/// )?;
471/// let row_sums = x.reduce_sum(&[1], &mut backend).unwrap();
472/// assert_eq!(row_sums.host_data()?, &[6.0, 15.0]);
473/// # Ok::<(), tenferro_runtime::Error>(())
474/// ```
475fn reduce_sum<T: TensorScalar>(
476    input: &TypedTensor<T>,
477    axes: &[usize],
478    backend: &mut impl TensorBackend,
479) -> Result<TypedTensor<T>> {
480    let out =
481        backend.with_backend_session(|exec| exec.reduce_sum_read(T::tensor_read(input), axes))?;
482    into_typed_result("reduce_sum", out)
483}
484
485/// Reshape a typed tensor through the backend structural operation.
486///
487/// # Examples
488///
489/// ```rust
490/// # use tenferro_cpu::CpuBackend;
491/// use tenferro_runtime::{TypedTensor, TypedTensorOpsExt};
492/// # let mut backend = CpuBackend::new();
493/// let x = TypedTensor::<f64>::from_vec_col_major(vec![2, 3], vec![1.0; 6]).unwrap();
494/// let y = x.reshape(&[3, 2], &mut backend).unwrap();
495/// assert_eq!(y.shape(), &[3, 2]);
496/// ```
497fn reshape<T: TensorScalar>(
498    input: &TypedTensor<T>,
499    shape: &[usize],
500    backend: &mut impl TensorBackend,
501) -> Result<TypedTensor<T>> {
502    let out =
503        backend.with_backend_session(|exec| exec.reshape_read(T::tensor_read(input), shape))?;
504    into_typed_result("reshape", out)
505}
506
507/// Permute typed tensor axes through the backend structural operation.
508///
509/// # Examples
510///
511/// ```rust
512/// # use tenferro_cpu::CpuBackend;
513/// use tenferro_runtime::{TypedTensor, TypedTensorOpsExt};
514/// # let mut backend = CpuBackend::new();
515/// let x = TypedTensor::<f64>::from_vec_col_major(vec![2, 3], vec![1.0; 6]).unwrap();
516/// let y = x.transpose(&[1, 0], &mut backend).unwrap();
517/// assert_eq!(y.shape(), &[3, 2]);
518/// ```
519fn transpose<T: TensorScalar>(
520    input: &TypedTensor<T>,
521    perm: &[usize],
522    backend: &mut impl TensorBackend,
523) -> Result<TypedTensor<T>> {
524    let out =
525        backend.with_backend_session(|exec| exec.transpose_read(T::tensor_read(input), perm))?;
526    into_typed_result("transpose", out)
527}
528
529/// Broadcast a typed tensor into a larger shape.
530///
531/// `dims` maps each input axis to its output axis, following the concrete
532/// backend `broadcast_in_dim` contract.
533///
534/// # Examples
535///
536/// ```rust
537/// # use tenferro_cpu::CpuBackend;
538/// use tenferro_runtime::{TypedTensor, TypedTensorOpsExt};
539/// # let mut backend = CpuBackend::new();
540/// let row = TypedTensor::<f64>::from_vec_col_major(vec![3], vec![1.0, 2.0, 3.0]).unwrap();
541/// let matrix = row.broadcast_in_dim(&[2, 3], &[1], &mut backend).unwrap();
542/// assert_eq!(matrix.shape(), &[2, 3]);
543/// ```
544fn broadcast_in_dim<T: TensorScalar>(
545    input: &TypedTensor<T>,
546    shape: &[usize],
547    dims: &[usize],
548    backend: &mut impl TensorBackend,
549) -> Result<TypedTensor<T>> {
550    let out = backend.with_backend_session(|exec| {
551        exec.broadcast_in_dim_read(T::tensor_read(input), shape, dims)
552    })?;
553    into_typed_result("broadcast_in_dim", out)
554}
555
556enum ReadInput<'a> {
557    Borrowed(TensorRead<'a>),
558    Owned(Tensor),
559}
560
561impl ReadInput<'_> {
562    fn tensor_read(&self) -> TensorRead<'_> {
563        match self {
564            Self::Borrowed(read) => read.clone(),
565            Self::Owned(tensor) => TensorRead::from_tensor(tensor),
566        }
567    }
568}
569
570fn broadcast_binary_read<'a, T: TensorScalar>(
571    lhs: &'a TypedTensor<T>,
572    rhs: &'a TypedTensor<T>,
573    backend: &mut impl TensorBackend,
574) -> Result<(ReadInput<'a>, ReadInput<'a>)> {
575    let shape = broadcast_shape(lhs.shape(), rhs.shape()).map_err(broadcast_error)?;
576    Ok((
577        broadcast_to_read(lhs, &shape, backend)?,
578        broadcast_to_read(rhs, &shape, backend)?,
579    ))
580}
581
582fn broadcast_ternary_read<'a, C: TensorScalar, T: TensorScalar>(
583    first: &'a TypedTensor<C>,
584    second: &'a TypedTensor<T>,
585    third: &'a TypedTensor<T>,
586    backend: &mut impl TensorBackend,
587) -> Result<(ReadInput<'a>, ReadInput<'a>, ReadInput<'a>)> {
588    let shape = broadcast_shapes([first.shape(), second.shape(), third.shape()])
589        .map_err(broadcast_error)?;
590    Ok((
591        broadcast_to_read(first, &shape, backend)?,
592        broadcast_to_read(second, &shape, backend)?,
593        broadcast_to_read(third, &shape, backend)?,
594    ))
595}
596
597fn broadcast_to_read<'a, T: TensorScalar>(
598    input: &'a TypedTensor<T>,
599    target_shape: &[usize],
600    backend: &mut impl TensorBackend,
601) -> Result<ReadInput<'a>> {
602    if input.shape() == target_shape {
603        return Ok(ReadInput::Borrowed(T::tensor_read(input)));
604    }
605
606    let plan = broadcast_input_plan(input.shape(), target_shape).map_err(broadcast_error)?;
607    let source = if plan.source_shape == input.shape() {
608        ReadInput::Borrowed(T::tensor_read(input))
609    } else {
610        let reshaped = backend.with_backend_session(|exec| {
611            exec.reshape_read(T::tensor_read(input), &plan.source_shape)
612        })?;
613        ReadInput::Owned(reshaped)
614    };
615    let out = backend.with_backend_session(|exec| {
616        exec.broadcast_in_dim_read(source.tensor_read(), target_shape, &plan.dims)
617    })?;
618    Ok(ReadInput::Owned(out))
619}
620
621fn broadcast_error(err: impl std::fmt::Display) -> Error {
622    Error::backend_failure("broadcast", err.to_string())
623}
624
625fn into_typed_result<T: TensorScalar>(op: &'static str, tensor: Tensor) -> Result<TypedTensor<T>> {
626    let actual = tensor.dtype();
627    T::into_typed(tensor).map_err(|_| Error::DTypeMismatch {
628        op,
629        lhs: T::dtype(),
630        rhs: actual,
631    })
632}