Skip to main content

oxmera_tensor/
tensor.rs

1//! The tensor value: a layout over shared storage, with an optional
2//! autograd tape node.
3
4use std::sync::{Arc, Mutex};
5
6use oxmera_core::layout::contiguous_strides;
7use oxmera_core::{DType, Device, Error, Layout, Result, Shape, Strides};
8use rand::SeedableRng;
9use rand_distr::Distribution;
10
11use crate::autograd::{AutogradMeta, GradFn};
12use crate::backend::backend_for;
13use crate::storage::{CpuStorage, Storage};
14
15/// A tensor: shared storage viewed through a layout.
16///
17/// Cloning a tensor is cheap — it clones the layout and bumps the storage
18/// refcount, never the data. View operations (`reshape`, `permute`,
19/// `narrow`, …) produce new tensors over the same storage whenever the
20/// layout arithmetic allows it.
21#[derive(Debug, Clone)]
22pub struct Tensor {
23    storage: Arc<Storage>,
24    layout: Layout,
25    autograd: Option<Arc<AutogradMeta>>,
26}
27
28impl Tensor {
29    // ---- construction ---------------------------------------------------
30
31    /// A tensor over existing storage with an explicit layout.
32    ///
33    /// Errors when the layout addresses elements outside the storage.
34    pub fn from_storage(storage: Arc<Storage>, layout: Layout) -> Result<Self> {
35        let needed = max_addressed(&layout);
36        let available = storage_len(&storage);
37        if needed > available {
38            return Err(Error::InvalidArgument {
39                op: "Tensor::from_storage",
40                detail: format!("layout addresses {needed} elements, storage holds {available}"),
41            });
42        }
43        Ok(Self {
44            storage,
45            layout,
46            autograd: None,
47        })
48    }
49
50    /// A contiguous CPU tensor holding `data` with shape `shape`.
51    ///
52    /// Errors when `data.len()` does not equal `shape.numel()`.
53    pub fn from_vec_f32(data: Vec<f32>, shape: impl Into<Shape>) -> Result<Self> {
54        let shape = shape.into();
55        let numel = checked_numel(&shape, "Tensor::from_vec_f32")?;
56        if data.len() != numel {
57            return Err(Error::ShapeMismatch {
58                expected: Shape::from([data.len()]),
59                got: shape,
60                op: "Tensor::from_vec_f32",
61            });
62        }
63        Ok(Self {
64            storage: Arc::new(Storage::from_f32_vec(data)),
65            layout: Layout::contiguous(shape),
66            autograd: None,
67        })
68    }
69
70    /// A contiguous CPU tensor copying `data` with shape `shape`.
71    pub fn from_slice(data: &[f32], shape: impl Into<Shape>) -> Result<Self> {
72        Self::from_vec_f32(data.to_vec(), shape)
73    }
74
75    /// A contiguous CPU `I64` tensor holding `data` (indices, targets).
76    pub fn from_vec_i64(data: Vec<i64>, shape: impl Into<Shape>) -> Result<Self> {
77        let shape = shape.into();
78        let numel = checked_numel(&shape, "Tensor::from_vec_i64")?;
79        if data.len() != numel {
80            return Err(Error::ShapeMismatch {
81                expected: Shape::from([data.len()]),
82                got: shape,
83                op: "Tensor::from_vec_i64",
84            });
85        }
86        Ok(Self {
87            storage: Arc::new(Storage::from_i64_vec(data)),
88            layout: Layout::contiguous(shape),
89            autograd: None,
90        })
91    }
92
93    /// A CPU tensor of zeros.
94    pub fn zeros(shape: impl Into<Shape>) -> Self {
95        let shape = shape.into();
96        let numel = shape.numel();
97        Self::from_vec_f32(vec![0.0; numel], shape).expect("lengths match by construction")
98    }
99
100    /// A CPU tensor of ones.
101    pub fn ones(shape: impl Into<Shape>) -> Self {
102        let shape = shape.into();
103        let numel = shape.numel();
104        Self::from_vec_f32(vec![1.0; numel], shape).expect("lengths match by construction")
105    }
106
107    /// A CPU tensor filled with `value`.
108    pub fn full(shape: impl Into<Shape>, value: f32) -> Self {
109        let shape = shape.into();
110        let numel = shape.numel();
111        Self::from_vec_f32(vec![value; numel], shape).expect("lengths match by construction")
112    }
113
114    /// A rank-0 scalar tensor.
115    pub fn scalar(value: f32) -> Self {
116        Self::from_vec_f32(vec![value], Shape::from([])).expect("scalar always fits")
117    }
118
119    /// Standard-normal random CPU tensor, seeded from the OS.
120    pub fn randn(shape: impl Into<Shape>) -> Self {
121        Self::randn_with_seed(shape, rand::random())
122    }
123
124    /// Standard-normal random CPU tensor with a fixed seed, for
125    /// reproducible tests and examples.
126    pub fn randn_with_seed(shape: impl Into<Shape>, seed: u64) -> Self {
127        let shape = shape.into();
128        let numel = shape.numel();
129        let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
130        let normal = rand_distr::StandardNormal;
131        let data: Vec<f32> = (0..numel).map(|_| normal.sample(&mut rng)).collect();
132        Self::from_vec_f32(data, shape).expect("lengths match by construction")
133    }
134
135    // ---- accessors -------------------------------------------------------
136
137    /// The shape of this view.
138    pub fn shape(&self) -> &Shape {
139        &self.layout.shape
140    }
141
142    /// The dimension extents, outermost first.
143    pub fn dims(&self) -> &[usize] {
144        self.layout.shape.dims()
145    }
146
147    /// The rank (number of dimensions).
148    pub fn ndim(&self) -> usize {
149        self.layout.shape.ndim()
150    }
151
152    /// The total number of elements.
153    pub fn numel(&self) -> usize {
154        self.layout.shape.numel()
155    }
156
157    /// The full layout of this view.
158    pub fn layout(&self) -> &Layout {
159        &self.layout
160    }
161
162    /// The element type.
163    pub fn dtype(&self) -> DType {
164        self.storage.dtype()
165    }
166
167    /// The device the storage lives on.
168    pub fn device(&self) -> Device {
169        self.storage.device()
170    }
171
172    /// The shared storage behind this view.
173    pub fn storage(&self) -> &Arc<Storage> {
174        &self.storage
175    }
176
177    // ---- element access (CPU) --------------------------------------------
178
179    /// The element at a logical index, as `f32`.
180    ///
181    /// Errors on rank mismatch, out-of-bounds, non-float dtype, or non-CPU
182    /// storage.
183    pub fn get_f32(&self, index: &[usize]) -> Result<f32> {
184        let offset = self.layout.offset_of(index)?;
185        Ok(self.storage.cpu()?.f32s()?[offset])
186    }
187
188    /// The element at a logical index, as `i64`.
189    pub fn get_i64(&self, index: &[usize]) -> Result<i64> {
190        let offset = self.layout.offset_of(index)?;
191        Ok(self.storage.cpu()?.i64s()?[offset])
192    }
193
194    /// Every element in logical (row-major) order, as `f32`, from CPU
195    /// storage.
196    pub fn to_vec_f32(&self) -> Result<Vec<f32>> {
197        let src = self.storage.cpu()?.f32s()?;
198        Ok(gather_logical(src, &self.layout))
199    }
200
201    /// Every element in logical (row-major) order, as `i64`.
202    pub fn to_vec_i64(&self) -> Result<Vec<i64>> {
203        let src = self.storage.cpu()?.i64s()?;
204        Ok(gather_logical(src, &self.layout))
205    }
206
207    // ---- views -----------------------------------------------------------
208
209    fn view(&self, layout: Layout) -> Self {
210        Self {
211            storage: Arc::clone(&self.storage),
212            layout,
213            autograd: None,
214        }
215    }
216
217    /// A view (or copy, when this view is not contiguous) with the same
218    /// elements in a new shape.
219    pub fn reshape(&self, shape: impl Into<Shape>) -> Result<Self> {
220        let shape = shape.into();
221        if checked_numel(&shape, "reshape")? != self.numel() {
222            return Err(Error::ShapeMismatch {
223                expected: self.shape().clone(),
224                got: shape,
225                op: "reshape",
226            });
227        }
228        let base = if self.layout.is_contiguous() {
229            self.clone()
230        } else {
231            self.contiguous_data()?
232        };
233        let layout = Layout {
234            strides: contiguous_strides(&shape),
235            shape,
236            offset: base.layout.offset,
237        };
238        let out = base.view(layout);
239        Ok(crate::ops::record_view(self, out, ViewKind::Reshape))
240    }
241
242    /// A view with dimensions reordered by `perm` (a permutation of
243    /// `0..ndim`).
244    pub fn permute(&self, perm: &[usize]) -> Result<Self> {
245        let n = self.ndim();
246        if perm.len() != n || {
247            let mut seen = vec![false; n];
248            perm.iter()
249                .any(|&p| p >= n || std::mem::replace(&mut seen[p], true))
250        } {
251            return Err(Error::InvalidArgument {
252                op: "permute",
253                detail: format!("{perm:?} is not a permutation of 0..{n}"),
254            });
255        }
256        let dims = self.dims();
257        let strides = self.layout.strides.values();
258        let new_dims: Vec<usize> = perm.iter().map(|&p| dims[p]).collect();
259        let new_strides: Vec<isize> = perm.iter().map(|&p| strides[p]).collect();
260        let layout = Layout {
261            shape: Shape::new(new_dims),
262            strides: Strides::new(new_strides),
263            offset: self.layout.offset,
264        };
265        let out = self.view(layout);
266        Ok(crate::ops::record_view(
267            self,
268            out,
269            ViewKind::Permute(perm.to_vec()),
270        ))
271    }
272
273    /// A view with dimensions `d0` and `d1` swapped.
274    pub fn transpose(&self, d0: usize, d1: usize) -> Result<Self> {
275        let mut perm: Vec<usize> = (0..self.ndim()).collect();
276        if d0 >= perm.len() || d1 >= perm.len() {
277            return Err(Error::InvalidArgument {
278                op: "transpose",
279                detail: format!("dims ({d0}, {d1}) out of range for rank {}", perm.len()),
280            });
281        }
282        perm.swap(d0, d1);
283        self.permute(&perm)
284    }
285
286    /// The matrix transpose: the last two dimensions swapped.
287    pub fn t(&self) -> Result<Self> {
288        let n = self.ndim();
289        if n < 2 {
290            return Err(Error::InvalidArgument {
291                op: "t",
292                detail: format!("needs rank >= 2, got {n}"),
293            });
294        }
295        self.transpose(n - 2, n - 1)
296    }
297
298    /// A view of `len` elements of dimension `dim` starting at `start`.
299    pub fn narrow(&self, dim: usize, start: usize, len: usize) -> Result<Self> {
300        let dims = self.dims();
301        if dim >= dims.len() || start + len > dims[dim] {
302            return Err(Error::InvalidArgument {
303                op: "narrow",
304                detail: format!(
305                    "dim {dim}, range {start}..{} against shape {:?}",
306                    start + len,
307                    self.shape()
308                ),
309            });
310        }
311        let mut new_dims = dims.to_vec();
312        new_dims[dim] = len;
313        let strides = self.layout.strides.values().to_vec();
314        let offset = (self.layout.offset as isize + start as isize * strides[dim]) as usize;
315        let layout = Layout {
316            shape: Shape::new(new_dims),
317            strides: Strides::new(strides),
318            offset,
319        };
320        let out = self.view(layout);
321        Ok(crate::ops::record_view(
322            self,
323            out,
324            ViewKind::Narrow { dim, start, len },
325        ))
326    }
327
328    /// A view of `range` along `dim` — sugar over [`Tensor::narrow`].
329    pub fn slice(&self, dim: usize, range: std::ops::Range<usize>) -> Result<Self> {
330        let len = range.end.saturating_sub(range.start);
331        self.narrow(dim, range.start, len)
332    }
333
334    /// A zero-copy broadcast view to `shape` (stride 0 on expanded axes).
335    pub fn broadcast_to(&self, shape: impl Into<Shape>) -> Result<Self> {
336        let shape = shape.into();
337        checked_numel(&shape, "broadcast_to")?;
338        let layout = broadcast_layout(&self.layout, &shape)?;
339        let out = self.view(layout);
340        Ok(crate::ops::record_view(self, out, ViewKind::Broadcast))
341    }
342
343    /// A broadcast view that records nothing on the tape — backend
344    /// plumbing; prefer [`Tensor::broadcast_to`] in user code.
345    pub fn broadcast_view(&self, shape: &Shape) -> Result<Self> {
346        checked_numel(shape, "broadcast_view")?;
347        let layout = broadcast_layout(&self.layout, shape)?;
348        Ok(self.view(layout))
349    }
350
351    /// A view with a new size-1 dimension inserted at `dim`.
352    pub fn unsqueeze(&self, dim: usize) -> Result<Self> {
353        let mut dims = self.dims().to_vec();
354        if dim > dims.len() {
355            return Err(Error::InvalidArgument {
356                op: "unsqueeze",
357                detail: format!("dim {dim} out of range for rank {}", dims.len()),
358            });
359        }
360        dims.insert(dim, 1);
361        let mut strides = self.layout.strides.values().to_vec();
362        strides.insert(dim, 0);
363        let layout = Layout {
364            shape: Shape::new(dims),
365            strides: Strides::new(strides),
366            offset: self.layout.offset,
367        };
368        let out = self.view(layout);
369        Ok(crate::ops::record_view(self, out, ViewKind::Reshape))
370    }
371
372    /// This tensor's elements, in logical order, in fresh contiguous
373    /// storage on the same device. A no-op clone when already contiguous.
374    pub fn contiguous(&self) -> Result<Self> {
375        if self.layout.is_contiguous() && self.layout.offset == 0 {
376            return Ok(self.clone());
377        }
378        let out = self.contiguous_data()?;
379        Ok(crate::ops::record_view(self, out, ViewKind::Contiguous))
380    }
381
382    /// The contiguous copy without autograd recording — backend plumbing;
383    /// prefer [`Tensor::contiguous`] in user code.
384    pub fn contiguous_untracked(&self) -> Result<Self> {
385        self.contiguous_data()
386    }
387
388    /// The contiguous copy without autograd recording (plumbing).
389    pub(crate) fn contiguous_data_crate(&self) -> Result<Self> {
390        self.contiguous_data()
391    }
392
393    /// The contiguous copy without autograd recording (plumbing).
394    pub(crate) fn contiguous_data(&self) -> Result<Self> {
395        match self.device() {
396            Device::Cpu => {
397                let shape = self.shape().clone();
398                match self.storage.cpu()? {
399                    CpuStorage::F32(_) => Tensor::from_vec_f32(self.to_vec_f32()?, shape),
400                    CpuStorage::I64(_) => Tensor::from_vec_i64(self.to_vec_i64()?, shape),
401                    CpuStorage::U8(_) => Err(Error::UnsupportedDType {
402                        dtype: DType::U8,
403                        op: "contiguous",
404                    }),
405                }
406            }
407            device => backend_for(device)?.contiguous(self),
408        }
409    }
410
411    // ---- autograd surface -------------------------------------------------
412
413    /// Mark (or unmark) this tensor as a gradient-accumulating leaf, in
414    /// place, returning it for chaining.
415    pub fn requires_grad_(mut self, requires: bool) -> Self {
416        match (&self.autograd, requires) {
417            (Some(meta), _) if meta.grad_fn.is_none() => {
418                // Leaf: rebuild the meta with the new flag.
419                self.autograd = requires.then(|| {
420                    Arc::new(AutogradMeta {
421                        requires_grad: true,
422                        grad: Mutex::new(None),
423                        grad_fn: None,
424                    })
425                });
426            }
427            (Some(_), true) => { /* non-leaf already tracked; nothing to do */ }
428            (Some(_), false) => self.autograd = None,
429            (None, true) => {
430                self.autograd = Some(Arc::new(AutogradMeta {
431                    requires_grad: true,
432                    grad: Mutex::new(None),
433                    grad_fn: None,
434                }));
435            }
436            (None, false) => {}
437        }
438        self
439    }
440
441    /// Whether gradients **accumulate on this tensor** during `backward`
442    /// — true for leaves marked with [`requires_grad_`](Self::requires_grad_).
443    ///
444    /// This answers a narrower question than PyTorch's `requires_grad`:
445    /// a tensor *computed from* such a leaf is on the tape but does not
446    /// accumulate a gradient of its own, so it reports `false` here. Ask
447    /// [`is_tracked`](Self::is_tracked) for "is this on the graph at all".
448    pub fn requires_grad(&self) -> bool {
449        self.autograd.as_ref().is_some_and(|m| m.requires_grad)
450    }
451
452    /// Whether this tensor participates in the autograd tape at all —
453    /// true for a leaf that requires grad and for anything computed from
454    /// one while recording was enabled; false for constants and for
455    /// everything produced under [`no_grad`](crate::autograd::no_grad).
456    ///
457    /// This is the predicate that observes `no_grad`:
458    ///
459    /// ```
460    /// use oxmera_tensor::tensor::Tensor;
461    /// use oxmera_tensor::autograd::no_grad;
462    ///
463    /// let a = Tensor::from_slice(&[1.0, 2.0], [2]).unwrap().requires_grad_(true);
464    /// assert!(a.mul_scalar(3.0).unwrap().is_tracked());
465    /// assert!(!no_grad(|| a.mul_scalar(3.0).unwrap()).is_tracked());
466    /// ```
467    pub fn is_tracked(&self) -> bool {
468        self.autograd.is_some()
469    }
470
471    /// The accumulated gradient, if a backward pass has produced one.
472    pub fn grad(&self) -> Option<Tensor> {
473        self.autograd
474            .as_ref()
475            .and_then(|m| m.grad.lock().expect("grad mutex poisoned").clone())
476    }
477
478    /// Clear this tensor's accumulated gradient.
479    pub fn zero_grad(&self) {
480        if let Some(meta) = &self.autograd {
481            *meta.grad.lock().expect("grad mutex poisoned") = None;
482        }
483    }
484
485    /// The same view without any tape connection.
486    pub fn detach(&self) -> Self {
487        Self {
488            storage: Arc::clone(&self.storage),
489            layout: self.layout.clone(),
490            autograd: None,
491        }
492    }
493
494    /// Propagate gradients from this scalar through the recorded tape.
495    ///
496    /// Errors when the tensor is not a scalar; use
497    /// [`Tensor::backward_with`] to seed a non-scalar output.
498    pub fn backward(&self) -> Result<()> {
499        if self.numel() != 1 {
500            return Err(Error::InvalidArgument {
501                op: "backward",
502                detail: format!(
503                    "output has {} elements; seed a non-scalar with backward_with",
504                    self.numel()
505                ),
506            });
507        }
508        // The seed lives where the loss lives: a CPU seed against a
509        // device-resident graph failed at the first VJP with a
510        // DeviceMismatch (found by the CUDA backend's end-to-end test, and
511        // latent on Metal).
512        let seed = Tensor::ones(self.shape().clone()).to_device(self.device())?;
513        self.backward_with(seed)
514    }
515
516    /// Propagate gradients seeding this tensor's gradient with `seed`.
517    pub fn backward_with(&self, seed: Tensor) -> Result<()> {
518        let seed = if seed.device() == self.device() {
519            seed
520        } else {
521            seed.to_device(self.device())?
522        };
523        crate::autograd::run_backward(self, seed)
524    }
525
526    pub(crate) fn autograd_meta(&self) -> Option<Arc<AutogradMeta>> {
527        self.autograd.clone()
528    }
529
530    /// Attach a tape node to this tensor (used by the op layer).
531    pub(crate) fn with_grad_fn(mut self, grad_fn: GradFn) -> Self {
532        self.autograd = Some(Arc::new(AutogradMeta {
533            requires_grad: false,
534            grad: Mutex::new(None),
535            grad_fn: Some(grad_fn),
536        }));
537        self
538    }
539}
540
541/// The internal view taxonomy the op layer uses to build view VJPs.
542#[derive(Debug, Clone)]
543pub(crate) enum ViewKind {
544    /// Reshape/unsqueeze: gradient reshapes back to the input shape.
545    Reshape,
546    /// Permute by this permutation: gradient permutes by the inverse.
547    Permute(Vec<usize>),
548    /// Narrow: gradient scatters back into zeros of the input shape.
549    Narrow {
550        /// Narrowed dimension.
551        dim: usize,
552        /// Range start.
553        start: usize,
554        /// Range length.
555        len: usize,
556    },
557    /// Broadcast view: gradient sum-reduces back to the input shape.
558    Broadcast,
559    /// Contiguous copy: gradient passes through (reshaped if needed).
560    Contiguous,
561}
562
563fn storage_len(storage: &Storage) -> usize {
564    match storage.data() {
565        crate::storage::StorageData::Cpu(c) => c.len(),
566        #[cfg(target_os = "macos")]
567        crate::storage::StorageData::Metal(b) => {
568            b.buffer().length() as usize / storage.dtype().size_in_bytes()
569        }
570        crate::storage::StorageData::Opaque(b) => b.len(),
571    }
572}
573
574fn max_addressed(layout: &Layout) -> usize {
575    if layout.shape.numel() == 0 {
576        return 0;
577    }
578    let mut max = layout.offset as isize;
579    for (&d, &s) in layout.shape.dims().iter().zip(layout.strides.values()) {
580        if d > 1 && s > 0 {
581            max += (d as isize - 1) * s;
582        }
583    }
584    (max + 1) as usize
585}
586
587/// Broadcast `layout` to `target`, stride 0 on expanded axes.
588pub(crate) fn broadcast_layout(layout: &Layout, target: &Shape) -> Result<Layout> {
589    let src = layout.shape.dims();
590    let dst = target.dims();
591    if dst.len() < src.len() {
592        return Err(Error::BroadcastIncompatible {
593            lhs: layout.shape.clone(),
594            rhs: target.clone(),
595        });
596    }
597    let lead = dst.len() - src.len();
598    let mut strides = vec![0isize; dst.len()];
599    for i in 0..src.len() {
600        let (s, d) = (src[i], dst[lead + i]);
601        if s == d {
602            strides[lead + i] = layout.strides.values()[i];
603        } else if s == 1 {
604            strides[lead + i] = 0;
605        } else {
606            return Err(Error::BroadcastIncompatible {
607                lhs: layout.shape.clone(),
608                rhs: target.clone(),
609            });
610        }
611    }
612    Ok(Layout {
613        shape: target.clone(),
614        strides: Strides::new(strides),
615        offset: layout.offset,
616    })
617}
618
619/// Gather a strided view's elements into logical row-major order.
620pub(crate) fn gather_logical<T: Copy>(src: &[T], layout: &Layout) -> Vec<T> {
621    let numel = layout.shape.numel();
622    let mut out = Vec::with_capacity(numel);
623    if numel == 0 {
624        return out;
625    }
626    let dims = layout.shape.dims();
627    if layout.is_contiguous() {
628        let start = layout.offset;
629        out.extend_from_slice(&src[start..start + numel]);
630        return out;
631    }
632    let strides = layout.strides.values();
633    // Row fast path: when the innermost stride is 1 the row is a slice
634    // copy; when it is 0 (a broadcast dimension being materialized) the
635    // row is one value repeated. Only a genuinely strided innermost
636    // dimension — a transposed view — falls through to the odometer.
637    let ndim = dims.len();
638    let inner = dims[ndim - 1];
639    let inner_stride = strides[ndim - 1];
640    if inner > 0 && (inner_stride == 0 || inner_stride == 1) {
641        let outer = Layout {
642            shape: Shape::new(dims[..ndim - 1].to_vec()),
643            strides: Strides::new(strides[..ndim - 1].to_vec()),
644            offset: layout.offset,
645        };
646        let mut walker = crate::cpu_iter::OffsetWalker::at(&outer, 0);
647        for _ in 0..numel / inner {
648            let base = walker.next_offset();
649            if inner_stride == 1 {
650                out.extend_from_slice(&src[base..base + inner]);
651            } else {
652                out.resize(out.len() + inner, src[base]);
653            }
654        }
655        return out;
656    }
657    let mut index = vec![0usize; dims.len()];
658    let mut offset = layout.offset as isize;
659    loop {
660        out.push(src[offset as usize]);
661        // Odometer increment, last dimension fastest, offset updated
662        // incrementally.
663        let mut d = dims.len();
664        loop {
665            if d == 0 {
666                return out;
667            }
668            d -= 1;
669            index[d] += 1;
670            offset += strides[d];
671            if index[d] < dims[d] {
672                break;
673            }
674            offset -= dims[d] as isize * strides[d];
675            index[d] = 0;
676        }
677        if out.len() == numel {
678            return out;
679        }
680    }
681}
682
683/// The element count of a caller-supplied shape, or a typed error when it
684/// does not fit in `usize` (see [`Shape::checked_numel`]).
685fn checked_numel(shape: &Shape, op: &'static str) -> Result<usize> {
686    shape.checked_numel().ok_or_else(|| Error::InvalidArgument {
687        op,
688        detail: format!(
689            "shape {:?} has more elements than fit in usize",
690            shape.dims()
691        ),
692    })
693}