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