1use oxmera_core::{DType, Device, Error, Result, Shape};
7
8use crate::autograd::{GradFn, is_recording};
9use crate::backend::{Backend, BinaryOp, ReduceOp, UnaryOp, backend_for};
10use crate::tensor::{Tensor, ViewKind};
11
12use std::sync::Arc;
13
14fn same_device(a: &Tensor, b: &Tensor, op: &'static str) -> Result<Device> {
15 if a.device() != b.device() {
16 return Err(Error::DeviceMismatch {
17 lhs: a.device(),
18 rhs: b.device(),
19 op,
20 });
21 }
22 Ok(a.device())
23}
24
25fn record(
26 out: Tensor,
27 inputs: Vec<Tensor>,
28 vjp: impl Fn(&Tensor) -> Result<Vec<Option<Tensor>>> + Send + Sync + 'static,
29) -> Tensor {
30 if is_recording() && inputs.iter().any(Tensor::is_tracked) {
31 out.with_grad_fn(GradFn {
32 inputs,
33 vjp: Box::new(vjp),
34 })
35 } else {
36 out
37 }
38}
39
40pub(crate) fn reduce_to_shape(grad: &Tensor, shape: &Shape) -> Result<Tensor> {
43 if grad.shape() == shape {
44 return Ok(grad.clone());
45 }
46 let gdims = grad.dims().to_vec();
47 let tdims = shape.dims();
48 let lead = gdims.len() - tdims.len();
49 let mut axes: Vec<usize> = (0..lead).collect();
50 for (i, &td) in tdims.iter().enumerate() {
51 if td == 1 && gdims[lead + i] != 1 {
52 axes.push(lead + i);
53 }
54 }
55 let reduced = if axes.is_empty() {
56 grad.clone()
57 } else {
58 grad.sum_keepdim(&axes, true)?
59 };
60 reduced.reshape(shape.clone())
61}
62
63pub(crate) fn record_view(input: &Tensor, out: Tensor, kind: ViewKind) -> Tensor {
65 let in_shape = input.shape().clone();
66 record(out, vec![input.clone()], move |g| {
67 let gi = match &kind {
68 ViewKind::Reshape | ViewKind::Contiguous => g.reshape(in_shape.clone())?,
69 ViewKind::Permute(perm) => {
70 let mut inverse = vec![0usize; perm.len()];
71 for (i, &p) in perm.iter().enumerate() {
72 inverse[p] = i;
73 }
74 g.permute(&inverse)?
75 }
76 ViewKind::Narrow { dim, start, len } => {
77 let indices: Vec<i64> = (*start..start + len).map(|i| i as i64).collect();
78 let indices = Tensor::from_vec_i64(indices, Shape::from([*len]))?;
79 Tensor::zeros(in_shape.clone())
80 .to_dtype(g.dtype())?
81 .to_device(g.device())?
82 .index_add(*dim, &indices, g)?
83 }
84 ViewKind::Broadcast => reduce_to_shape(g, &in_shape)?,
85 };
86 Ok(vec![Some(gi)])
87 })
88}
89
90impl Tensor {
91 fn backend(&self) -> Result<Arc<dyn Backend>> {
92 backend_for(self.device())
93 }
94
95 fn unary_op(&self, op: UnaryOp) -> Result<Tensor> {
98 let out = self.backend()?.unary(op, self)?;
99 let a = self.clone();
100 let o = out.clone();
101 Ok(record(out, vec![self.clone()], move |g| {
102 let gi = match op {
103 UnaryOp::Neg => g.neg()?,
104 UnaryOp::Exp => g.mul(&o)?,
105 UnaryOp::Ln => g.div(&a)?,
106 UnaryOp::Abs => {
107 let sign = a
108 .gt_mask(&Tensor::scalar_on(&a, 0.0)?)?
109 .sub(&Tensor::scalar_on(&a, 0.0)?.gt_mask(&a)?)?;
110 g.mul(&sign)?
111 }
112 UnaryOp::Sqrt => g.mul(&Tensor::scalar_on(&a, 0.5)?)?.div(&o)?,
113 UnaryOp::Sin => g.mul(&a.cos()?)?,
114 UnaryOp::Cos => g.mul(&a.sin()?.neg()?)?,
115 UnaryOp::Tanh => {
116 let one = Tensor::scalar_on(&a, 1.0)?;
117 g.mul(&one.sub(&o.mul(&o)?)?)?
118 }
119 UnaryOp::Relu => g.mul(&a.gt_mask(&Tensor::scalar_on(&a, 0.0)?)?)?,
120 UnaryOp::Gelu => {
121 let c = Tensor::scalar_on(&a, 0.797_884_6)?;
124 let k = Tensor::scalar_on(&a, 0.044_715)?;
125 let one = Tensor::scalar_on(&a, 1.0)?;
126 let half = Tensor::scalar_on(&a, 0.5)?;
127 let three_k = Tensor::scalar_on(&a, 3.0 * 0.044_715)?;
128 let x2 = a.mul(&a)?;
129 let u = c.mul(&a.add(&k.mul(&x2.mul(&a)?)?)?)?;
130 let t = u.tanh()?;
131 let sech2 = one.sub(&t.mul(&t)?)?;
132 let du = c.mul(&one.add(&three_k.mul(&x2)?)?)?;
133 let d = half
134 .mul(&one.add(&t)?)?
135 .add(&half.mul(&a)?.mul(&sech2)?.mul(&du)?)?;
136 g.mul(&d)?
137 }
138 UnaryOp::Sigmoid => {
139 let one = Tensor::scalar_on(&a, 1.0)?;
140 g.mul(&o)?.mul(&one.sub(&o)?)?
141 }
142 };
143 Ok(vec![Some(gi)])
144 }))
145 }
146
147 pub fn neg(&self) -> Result<Tensor> {
149 self.unary_op(UnaryOp::Neg)
150 }
151 pub fn exp(&self) -> Result<Tensor> {
153 self.unary_op(UnaryOp::Exp)
154 }
155 pub fn ln(&self) -> Result<Tensor> {
157 self.unary_op(UnaryOp::Ln)
158 }
159 pub fn abs(&self) -> Result<Tensor> {
161 self.unary_op(UnaryOp::Abs)
162 }
163 pub fn sqrt(&self) -> Result<Tensor> {
165 self.unary_op(UnaryOp::Sqrt)
166 }
167 pub fn sin(&self) -> Result<Tensor> {
169 self.unary_op(UnaryOp::Sin)
170 }
171 pub fn cos(&self) -> Result<Tensor> {
173 self.unary_op(UnaryOp::Cos)
174 }
175 pub fn tanh(&self) -> Result<Tensor> {
177 self.unary_op(UnaryOp::Tanh)
178 }
179 pub fn relu(&self) -> Result<Tensor> {
181 self.unary_op(UnaryOp::Relu)
182 }
183 pub fn gelu(&self) -> Result<Tensor> {
185 self.unary_op(UnaryOp::Gelu)
186 }
187 pub fn sigmoid(&self) -> Result<Tensor> {
189 self.unary_op(UnaryOp::Sigmoid)
190 }
191
192 pub fn scalar_on(like: &Tensor, value: f32) -> Result<Tensor> {
195 let s = match like.dtype() {
196 DType::F64 => Tensor::from_vec_f64(vec![f64::from(value)], Shape::from([]))?,
197 _ => Tensor::scalar(value),
198 };
199 s.to_device(like.device())
200 }
201
202 pub fn to_dtype(&self, dtype: DType) -> Result<Tensor> {
206 if self.dtype() == dtype {
207 return Ok(self.clone());
208 }
209 if self.device() != Device::Cpu {
210 return Err(Error::UnsupportedDType {
211 dtype,
212 op: "to_dtype (device tensors are f32; convert on the CPU)",
213 });
214 }
215 let shape = self.shape().clone();
216 let out = match (self.dtype(), dtype) {
217 (DType::F32, DType::F64) => Tensor::from_vec_f64(
218 self.to_vec_f32()?.into_iter().map(f64::from).collect(),
219 shape,
220 )?,
221 (DType::F64, DType::F32) => Tensor::from_vec_f32(
222 self.to_vec_f64()?.into_iter().map(|x| x as f32).collect(),
223 shape,
224 )?,
225 (DType::I64, DType::F32) => Tensor::from_vec_f32(
226 self.to_vec_i64()?.into_iter().map(|x| x as f32).collect(),
227 shape,
228 )?,
229 (DType::I64, DType::F64) => Tensor::from_vec_f64(
230 self.to_vec_i64()?.into_iter().map(|x| x as f64).collect(),
231 shape,
232 )?,
233 (_, to) => {
234 return Err(Error::UnsupportedDType {
235 dtype: to,
236 op: "to_dtype",
237 });
238 }
239 };
240 let from = self.dtype();
241 Ok(record(out, vec![self.clone()], move |g| {
242 Ok(vec![Some(g.to_dtype(from)?)])
243 }))
244 }
245
246 fn binary_op(&self, op: BinaryOp, rhs: &Tensor) -> Result<Tensor> {
249 let device = same_device(self, rhs, "binary")?;
250 let out = backend_for(device)?.binary(op, self, rhs)?;
251 let (a, b) = (self.clone(), rhs.clone());
252 let o = out.clone();
253 Ok(record(out, vec![self.clone(), rhs.clone()], move |g| {
254 let (ga, gb): (Option<Tensor>, Option<Tensor>) = match op {
255 BinaryOp::Add => (Some(g.clone()), Some(g.clone())),
256 BinaryOp::Sub => (Some(g.clone()), Some(g.neg()?)),
257 BinaryOp::Mul => (Some(g.mul_raw(&b)?), Some(g.mul_raw(&a)?)),
258 BinaryOp::Div => {
259 let ga = g.div_raw(&b)?;
260 let gb = g.mul_raw(&o)?.div_raw(&b)?.neg()?;
261 (Some(ga), Some(gb))
262 }
263 BinaryOp::Pow => {
264 let one = Tensor::scalar_on(&a, 1.0)?;
265 let ga = g.mul_raw(&b)?.mul_raw(&a.pow(&b.sub(&one)?)?)?;
266 let gb = g.mul_raw(&o)?.mul_raw(&a.ln()?)?;
267 (Some(ga), Some(gb))
268 }
269 BinaryOp::Maximum => {
276 let half = a.eq_mask(&b)?.mul_scalar(0.5)?;
277 let wa = a.gt_mask(&b)?.add(&half)?;
278 let one = Tensor::scalar_on(&a, 1.0)?;
279 let ga = g.mul_raw(&wa)?;
280 let gb = g.mul_raw(&one.sub(&wa)?)?;
281 (Some(ga), Some(gb))
282 }
283 BinaryOp::Minimum => {
284 let half = a.eq_mask(&b)?.mul_scalar(0.5)?;
285 let wa = b.gt_mask(&a)?.add(&half)?;
286 let one = Tensor::scalar_on(&a, 1.0)?;
287 let ga = g.mul_raw(&wa)?;
288 let gb = g.mul_raw(&one.sub(&wa)?)?;
289 (Some(ga), Some(gb))
290 }
291 BinaryOp::Gt | BinaryOp::Eq => (None, None),
292 };
293 let ga = match ga {
294 Some(t) => Some(reduce_to_shape(&t, a.shape())?),
295 None => None,
296 };
297 let gb = match gb {
298 Some(t) => Some(reduce_to_shape(&t, b.shape())?),
299 None => None,
300 };
301 Ok(vec![ga, gb])
302 }))
303 }
304
305 fn mul_raw(&self, rhs: &Tensor) -> Result<Tensor> {
308 let device = same_device(self, rhs, "mul")?;
309 backend_for(device)?.binary(BinaryOp::Mul, self, rhs)
310 }
311
312 fn div_raw(&self, rhs: &Tensor) -> Result<Tensor> {
313 let device = same_device(self, rhs, "div")?;
314 backend_for(device)?.binary(BinaryOp::Div, self, rhs)
315 }
316
317 pub fn add(&self, rhs: &Tensor) -> Result<Tensor> {
319 self.binary_op(BinaryOp::Add, rhs)
320 }
321 pub fn sub(&self, rhs: &Tensor) -> Result<Tensor> {
323 self.binary_op(BinaryOp::Sub, rhs)
324 }
325 pub fn mul(&self, rhs: &Tensor) -> Result<Tensor> {
327 self.binary_op(BinaryOp::Mul, rhs)
328 }
329 pub fn div(&self, rhs: &Tensor) -> Result<Tensor> {
331 self.binary_op(BinaryOp::Div, rhs)
332 }
333 pub fn pow(&self, rhs: &Tensor) -> Result<Tensor> {
335 self.binary_op(BinaryOp::Pow, rhs)
336 }
337 pub fn maximum(&self, rhs: &Tensor) -> Result<Tensor> {
339 self.binary_op(BinaryOp::Maximum, rhs)
340 }
341 pub fn minimum(&self, rhs: &Tensor) -> Result<Tensor> {
343 self.binary_op(BinaryOp::Minimum, rhs)
344 }
345 pub fn gt_mask(&self, rhs: &Tensor) -> Result<Tensor> {
347 self.binary_op(BinaryOp::Gt, rhs)
348 }
349 pub fn eq_mask(&self, rhs: &Tensor) -> Result<Tensor> {
351 self.binary_op(BinaryOp::Eq, rhs)
352 }
353
354 pub fn add_scalar(&self, s: f32) -> Result<Tensor> {
356 self.add(&Tensor::scalar_on(self, s)?)
357 }
358 pub fn mul_scalar(&self, s: f32) -> Result<Tensor> {
360 self.mul(&Tensor::scalar_on(self, s)?)
361 }
362
363 pub fn matmul(&self, rhs: &Tensor) -> Result<Tensor> {
384 let device = same_device(self, rhs, "matmul")?;
385 if self.ndim() > 3 || rhs.ndim() > 3 {
386 return self.matmul_lowered(rhs);
387 }
388 let out = backend_for(device)?.matmul(self, rhs)?;
389 let (a, b) = (self.clone(), rhs.clone());
390 Ok(record(out, vec![self.clone(), rhs.clone()], move |g| {
391 let ga = backend_for(g.device())?.matmul(g, &b.t()?)?;
394 let gb = backend_for(g.device())?.matmul(&a.t()?, g)?;
395 Ok(vec![
396 Some(reduce_to_shape(&ga, a.shape())?),
397 Some(reduce_to_shape(&gb, b.shape())?),
398 ])
399 }))
400 }
401
402 fn matmul_lowered(&self, rhs: &Tensor) -> Result<Tensor> {
405 let (ad, bd) = (self.dims(), rhs.dims());
406 if ad.len() < 2 || bd.len() < 2 {
407 return Err(Error::InvalidArgument {
408 op: "matmul",
409 detail: format!("operands need rank >= 2; got {}x{}", ad.len(), bd.len()),
410 });
411 }
412 let (m, k) = (ad[ad.len() - 2], ad[ad.len() - 1]);
413 let (kb, n) = (bd[bd.len() - 2], bd[bd.len() - 1]);
414 if k != kb {
415 return Err(Error::ShapeMismatch {
416 expected: Shape::new(bd[..bd.len() - 2].iter().copied().chain([k, n]).collect()),
417 got: rhs.shape().clone(),
418 op: "matmul",
419 });
420 }
421 let a_batch = Shape::new(ad[..ad.len() - 2].to_vec());
422 let b_batch = Shape::new(bd[..bd.len() - 2].to_vec());
423 let batch = oxmera_core::shape::broadcast_shapes(&a_batch, &b_batch).map_err(|_| {
424 Error::BroadcastIncompatible {
425 lhs: self.shape().clone(),
426 rhs: rhs.shape().clone(),
427 }
428 })?;
429 let batch_numel = batch.numel();
430 let lower = |t: &Tensor, own: &Shape, rows: usize, cols: usize| -> Result<Tensor> {
434 if own.numel() == 1 {
435 return t.reshape(Shape::from([rows, cols]));
436 }
437 let full: Vec<usize> = batch.dims().iter().copied().chain([rows, cols]).collect();
438 let expanded = if own.dims() == batch.dims() {
439 t.clone()
440 } else {
441 let lead = batch.ndim() - own.ndim();
444 let padded: Vec<usize> = std::iter::repeat_n(1usize, lead)
445 .chain(own.dims().iter().copied())
446 .chain([rows, cols])
447 .collect();
448 t.reshape(Shape::new(padded))?
449 .broadcast_to(Shape::new(full.clone()))?
450 .contiguous()?
451 };
452 expanded.reshape(Shape::from([batch_numel, rows, cols]))
453 };
454 let a3 = lower(self, &a_batch, m, k)?;
455 let b3 = lower(rhs, &b_batch, k, n)?;
456 let out = a3.matmul(&b3)?;
457 let out_shape: Vec<usize> = batch.dims().iter().copied().chain([m, n]).collect();
458 out.reshape(Shape::new(out_shape))
459 }
460
461 fn reduce_op(&self, op: ReduceOp, axes: &[usize], keepdim: bool) -> Result<Tensor> {
464 let axes = normalize_axes(axes, self.ndim(), "reduce")?;
465 let out = self.backend()?.reduce(op, self, &axes, keepdim)?;
466 let a = self.clone();
467 let o = out.clone();
468 let axes_c = axes.clone();
469 Ok(record(out, vec![self.clone()], move |g| {
470 let g_keep = if keepdim {
472 g.clone()
473 } else {
474 unsqueeze_axes(g, &axes_c)?
475 };
476 let gi = match op {
477 ReduceOp::Sum => g_keep.broadcast_to(a.shape().clone())?.contiguous()?,
478 ReduceOp::Max | ReduceOp::Min => {
479 let o_keep = if keepdim {
480 o.clone()
481 } else {
482 unsqueeze_axes(&o, &axes_c)?
483 };
484 let mask = a.eq_mask(&o_keep.broadcast_to(a.shape().clone())?)?;
485 let count = mask.sum_keepdim(&axes_c, true)?;
486 g_keep
487 .broadcast_to(a.shape().clone())?
488 .mul_raw(&mask)?
489 .div_raw(&count.broadcast_to(a.shape().clone())?.contiguous()?)?
490 }
491 };
492 Ok(vec![Some(gi)])
493 }))
494 }
495
496 pub fn sum(&self, axes: &[usize]) -> Result<Tensor> {
498 self.reduce_op(ReduceOp::Sum, axes, false)
499 }
500
501 pub fn sum_keepdim(&self, axes: &[usize], keepdim: bool) -> Result<Tensor> {
503 self.reduce_op(ReduceOp::Sum, axes, keepdim)
504 }
505
506 pub fn max(&self, axes: &[usize]) -> Result<Tensor> {
508 self.reduce_op(ReduceOp::Max, axes, false)
509 }
510
511 pub fn max_keepdim(&self, axes: &[usize], keepdim: bool) -> Result<Tensor> {
513 self.reduce_op(ReduceOp::Max, axes, keepdim)
514 }
515
516 pub fn min(&self, axes: &[usize]) -> Result<Tensor> {
518 self.reduce_op(ReduceOp::Min, axes, false)
519 }
520
521 pub fn mean(&self, axes: &[usize]) -> Result<Tensor> {
527 self.mean_keepdim(axes, false)
528 }
529
530 pub fn mean_keepdim(&self, axes: &[usize], keepdim: bool) -> Result<Tensor> {
550 let axes_n = normalize_axes(axes, self.ndim(), "mean")?;
551 let n: usize = axes_n.iter().map(|&ax| self.dims()[ax]).product();
552 if n == 0 {
553 let empty: Vec<usize> = axes_n
554 .iter()
555 .copied()
556 .filter(|&ax| self.dims()[ax] == 0)
557 .collect();
558 return Err(Error::InvalidArgument {
559 op: "mean",
560 detail: format!(
561 "dimension(s) {empty:?} have extent 0, so the mean would be 0/0; \
562 the mean of nothing is undefined — use sum() if an empty \
563 reduction should be 0, or guard the empty case at the call site"
564 ),
565 });
566 }
567 let summed = self.sum_keepdim(&axes_n, keepdim)?;
568 if summed.dtype() == DType::F64 {
569 let divisor = Tensor::from_vec_f64(vec![n as f64], Shape::from([]))?
573 .to_device(summed.device())?;
574 return summed.div(&divisor);
575 }
576 summed.mul_scalar(1.0 / n as f32)
577 }
578
579 pub fn argmax(&self, dim: usize, keepdim: bool) -> Result<Tensor> {
582 if dim >= self.ndim() {
583 return Err(Error::InvalidArgument {
584 op: "argmax",
585 detail: format!("dim {dim} out of range for rank {}", self.ndim()),
586 });
587 }
588 self.backend()?.argmax(self, dim, keepdim)
589 }
590
591 pub fn softmax(&self, dim: usize) -> Result<Tensor> {
593 let shifted = self.sub(&self.max_keepdim(&[dim], true)?.detach())?;
596 let e = shifted.exp()?;
597 let denom = e.sum_keepdim(&[dim], true)?;
598 e.div(&denom)
599 }
600
601 pub fn log_softmax(&self, dim: usize) -> Result<Tensor> {
603 let shifted = self.sub(&self.max_keepdim(&[dim], true)?.detach())?;
604 let lse = shifted.exp()?.sum_keepdim(&[dim], true)?.ln()?;
605 shifted.sub(&lse)
606 }
607
608 pub fn index_select(&self, dim: usize, indices: &Tensor) -> Result<Tensor> {
612 let out = dispatch_index(self, &[indices], |be, t, extra| {
613 be.index_select(t, dim, &extra[0])
614 })?;
615 let in_shape = self.shape().clone();
616 let idx = indices.clone();
617 Ok(record(out, vec![self.clone()], move |g| {
618 let zeros = Tensor::zeros(in_shape.clone())
619 .to_dtype(g.dtype())?
620 .to_device(g.device())?;
621 Ok(vec![Some(zeros.index_add(dim, &idx, g)?)])
622 }))
623 }
624
625 pub fn index_add(&self, dim: usize, indices: &Tensor, src: &Tensor) -> Result<Tensor> {
628 same_device(self, src, "index_add")?;
629 let out = dispatch_index(self, &[indices, src], |be, t, extra| {
630 be.index_add(t, dim, &extra[0], &extra[1])
631 })?;
632 let idx = indices.clone();
633 Ok(record(out, vec![self.clone(), src.clone()], move |g| {
634 Ok(vec![Some(g.clone()), Some(g.index_select(dim, &idx)?)])
635 }))
636 }
637
638 pub fn to_device(&self, device: Device) -> Result<Tensor> {
643 if self.device() == device {
644 return Ok(self.clone());
645 }
646 if self.dtype() == DType::F64 {
647 return Err(Error::UnsupportedDType {
648 dtype: DType::F64,
649 op: "to_device (f64 tensors live on the CPU; to_dtype(F32) first)",
650 });
651 }
652 let out = match (self.device(), device) {
653 (Device::Cpu, target) => backend_for(target)?.upload(&self.contiguous_data()?)?,
654 (_, Device::Cpu) => self.backend()?.download(self)?,
655 (_, target) => {
656 let host = self.backend()?.download(self)?;
657 backend_for(target)?.upload(&host)?
658 }
659 };
660 let source = self.device();
661 Ok(record(out, vec![self.clone()], move |g| {
662 Ok(vec![Some(g.to_device(source)?)])
663 }))
664 }
665}
666
667fn unsqueeze_axes(t: &Tensor, axes: &[usize]) -> Result<Tensor> {
670 let mut out = t.clone();
671 let mut sorted = axes.to_vec();
672 sorted.sort_unstable();
673 for &ax in &sorted {
674 out = out.unsqueeze(ax)?;
675 }
676 Ok(out)
677}
678
679fn normalize_axes(axes: &[usize], ndim: usize, op: &'static str) -> Result<Vec<usize>> {
681 let mut axes: Vec<usize> = if axes.is_empty() {
682 (0..ndim).collect()
683 } else {
684 axes.to_vec()
685 };
686 axes.sort_unstable();
687 axes.dedup();
688 if let Some(&bad) = axes.iter().find(|&&a| a >= ndim) {
689 return Err(Error::InvalidArgument {
690 op,
691 detail: format!("axis {bad} out of range for rank {ndim}"),
692 });
693 }
694 Ok(axes)
695}
696
697fn dispatch_index(
704 t: &Tensor,
705 extra: &[&Tensor],
706 f: impl Fn(&dyn Backend, &Tensor, &[Tensor]) -> Result<Tensor>,
707) -> Result<Tensor> {
708 let backend = backend_for(t.device())?;
709 let on_device: Vec<Tensor> = extra.iter().map(|e| (*e).clone()).collect();
710 match f(backend.as_ref(), t, &on_device) {
711 Err(Error::NotImplemented { .. }) if t.device() != Device::Cpu => {
712 let cpu = backend.download(t)?;
713 let cpu_extra: Vec<Tensor> = extra
714 .iter()
715 .map(|e| e.to_device(Device::Cpu))
716 .collect::<Result<_>>()?;
717 let cpu_backend = backend_for(Device::Cpu)?;
718 let out = f(cpu_backend.as_ref(), &cpu, &cpu_extra)?;
719 backend_for(t.device())?.upload(&out)
720 }
721 other => other,
722 }
723}