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(Clone)]
22pub struct Tensor {
23    storage: Arc<Storage>,
24    layout: Layout,
25    autograd: Option<Arc<AutogradMeta>>,
26}
27
28impl std::fmt::Debug for Tensor {
29    /// Prints only the tensor's metadata — shape, dtype, device and
30    /// whether it tracks gradients. Never the storage contents: a tensor
31    /// can hold gigabytes, and a derived `Debug` dumped all of it into
32    /// every log line and panic message.
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        f.debug_struct("Tensor")
35            .field("shape", self.shape())
36            .field("dtype", &self.dtype())
37            .field("device", &self.device())
38            .field("requires_grad", &self.requires_grad())
39            .finish()
40    }
41}
42
43impl Tensor {
44    // ---- construction ---------------------------------------------------
45
46    /// A tensor over existing storage with an explicit layout.
47    ///
48    /// Errors when the layout addresses elements outside the storage.
49    pub fn from_storage(storage: Arc<Storage>, layout: Layout) -> Result<Self> {
50        let available = storage_len(&storage) as isize;
51        let Some((lo, hi)) = addressed_bounds(&layout) else {
52            return Err(Error::InvalidArgument {
53                op: "Tensor::from_storage",
54                detail: "layout extents overflow the address space, so it cannot be proven \
55                         in bounds"
56                    .into(),
57            });
58        };
59        if lo < 0 || hi > available {
60            return Err(Error::InvalidArgument {
61                op: "Tensor::from_storage",
62                detail: format!(
63                    "layout addresses [{lo}, {hi}) but storage holds {available} elements"
64                ),
65            });
66        }
67        Ok(Self {
68            storage,
69            layout,
70            autograd: None,
71        })
72    }
73
74    /// A contiguous CPU tensor holding `data` with shape `shape`.
75    ///
76    /// Errors when `data.len()` does not equal `shape.numel()`.
77    pub fn from_vec_f32(data: Vec<f32>, shape: impl Into<Shape>) -> Result<Self> {
78        let shape = shape.into();
79        let numel = checked_numel(&shape, "Tensor::from_vec_f32")?;
80        if data.len() != numel {
81            return Err(Error::ShapeMismatch {
82                expected: Shape::from([data.len()]),
83                got: shape,
84                op: "Tensor::from_vec_f32",
85            });
86        }
87        Ok(Self {
88            storage: Arc::new(Storage::from_f32_vec(data)),
89            layout: Layout::contiguous(shape),
90            autograd: None,
91        })
92    }
93
94    /// A contiguous CPU tensor copying `data` with shape `shape`.
95    pub fn from_slice(data: &[f32], shape: impl Into<Shape>) -> Result<Self> {
96        Self::from_vec_f32(data.to_vec(), shape)
97    }
98
99    /// A contiguous CPU `F64` tensor holding `data`. `f64` tensors live on
100    /// the CPU (the GPU backends carry `f32`); every op the CPU backend
101    /// implements accepts them, and [`Tensor::to_dtype`] converts.
102    pub fn from_vec_f64(data: Vec<f64>, shape: impl Into<Shape>) -> Result<Self> {
103        let shape = shape.into();
104        let numel = checked_numel(&shape, "Tensor::from_vec_f64")?;
105        if data.len() != numel {
106            return Err(Error::ShapeMismatch {
107                expected: Shape::from([data.len()]),
108                got: shape,
109                op: "Tensor::from_vec_f64",
110            });
111        }
112        Ok(Self {
113            storage: Arc::new(Storage::from_f64_vec(data)),
114            layout: Layout::contiguous(shape),
115            autograd: None,
116        })
117    }
118
119    /// A contiguous CPU `I64` tensor holding `data` (indices, targets).
120    pub fn from_vec_i64(data: Vec<i64>, shape: impl Into<Shape>) -> Result<Self> {
121        let shape = shape.into();
122        let numel = checked_numel(&shape, "Tensor::from_vec_i64")?;
123        if data.len() != numel {
124            return Err(Error::ShapeMismatch {
125                expected: Shape::from([data.len()]),
126                got: shape,
127                op: "Tensor::from_vec_i64",
128            });
129        }
130        Ok(Self {
131            storage: Arc::new(Storage::from_i64_vec(data)),
132            layout: Layout::contiguous(shape),
133            autograd: None,
134        })
135    }
136
137    /// A CPU tensor of zeros.
138    ///
139    /// # Panics
140    /// Panics if the shape's element count overflows `usize`. Build the
141    /// shape from untrusted input through [`Tensor::try_zeros`] for a
142    /// typed error instead.
143    pub fn zeros(shape: impl Into<Shape>) -> Self {
144        Self::try_zeros(shape).expect("shape element count overflows usize")
145    }
146
147    /// A CPU tensor of zeros, or [`Error::InvalidArgument`] when the
148    /// shape's element count overflows `usize`.
149    pub fn try_zeros(shape: impl Into<Shape>) -> Result<Self> {
150        let shape = shape.into();
151        let numel = checked_numel(&shape, "Tensor::try_zeros")?;
152        Self::from_vec_f32(try_filled(numel, 0.0, "Tensor::try_zeros")?, shape)
153    }
154
155    /// A CPU tensor of ones.
156    ///
157    /// # Panics
158    /// Panics if the shape's element count overflows `usize`; see
159    /// [`Tensor::try_ones`].
160    pub fn ones(shape: impl Into<Shape>) -> Self {
161        Self::try_ones(shape).expect("shape element count overflows usize")
162    }
163
164    /// A CPU tensor of ones, or [`Error::InvalidArgument`] when the
165    /// shape's element count overflows `usize`.
166    pub fn try_ones(shape: impl Into<Shape>) -> Result<Self> {
167        let shape = shape.into();
168        let numel = checked_numel(&shape, "Tensor::try_ones")?;
169        Self::from_vec_f32(try_filled(numel, 1.0, "Tensor::try_ones")?, shape)
170    }
171
172    /// A CPU tensor filled with `value`.
173    ///
174    /// # Panics
175    /// Panics if the shape's element count overflows `usize`; see
176    /// [`Tensor::try_full`].
177    pub fn full(shape: impl Into<Shape>, value: f32) -> Self {
178        Self::try_full(shape, value).expect("shape element count overflows usize")
179    }
180
181    /// A CPU tensor filled with `value`, or [`Error::InvalidArgument`]
182    /// when the shape's element count overflows `usize`.
183    pub fn try_full(shape: impl Into<Shape>, value: f32) -> Result<Self> {
184        let shape = shape.into();
185        let numel = checked_numel(&shape, "Tensor::try_full")?;
186        Self::from_vec_f32(try_filled(numel, value, "Tensor::try_full")?, shape)
187    }
188
189    /// A rank-0 scalar tensor.
190    pub fn scalar(value: f32) -> Self {
191        Self::from_vec_f32(vec![value], Shape::from([])).expect("scalar always fits")
192    }
193
194    /// Standard-normal random CPU tensor, seeded from the OS.
195    pub fn randn(shape: impl Into<Shape>) -> Self {
196        Self::randn_with_seed(shape, rand::random())
197    }
198
199    /// Standard-normal random CPU tensor with a fixed seed, for
200    /// reproducible tests and examples.
201    pub fn randn_with_seed(shape: impl Into<Shape>, seed: u64) -> Self {
202        let shape = shape.into();
203        let numel = shape.numel();
204        let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
205        let normal = rand_distr::StandardNormal;
206        let data: Vec<f32> = (0..numel).map(|_| normal.sample(&mut rng)).collect();
207        Self::from_vec_f32(data, shape).expect("lengths match by construction")
208    }
209
210    // ---- accessors -------------------------------------------------------
211
212    /// The shape of this view.
213    pub fn shape(&self) -> &Shape {
214        &self.layout.shape
215    }
216
217    /// The dimension extents, outermost first.
218    pub fn dims(&self) -> &[usize] {
219        self.layout.shape.dims()
220    }
221
222    /// The rank (number of dimensions).
223    pub fn ndim(&self) -> usize {
224        self.layout.shape.ndim()
225    }
226
227    /// The total number of elements.
228    pub fn numel(&self) -> usize {
229        self.layout.shape.numel()
230    }
231
232    /// The full layout of this view.
233    pub fn layout(&self) -> &Layout {
234        &self.layout
235    }
236
237    /// The element type.
238    pub fn dtype(&self) -> DType {
239        self.storage.dtype()
240    }
241
242    /// The device the storage lives on.
243    pub fn device(&self) -> Device {
244        self.storage.device()
245    }
246
247    /// The shared storage behind this view.
248    pub fn storage(&self) -> &Arc<Storage> {
249        &self.storage
250    }
251
252    // ---- element access (CPU) --------------------------------------------
253
254    /// The element at a logical index, as `f32`.
255    ///
256    /// Errors on rank mismatch, out-of-bounds, non-float dtype, or non-CPU
257    /// storage.
258    pub fn get_f32(&self, index: &[usize]) -> Result<f32> {
259        let offset = self.layout.offset_of(index)?;
260        Ok(self.storage.cpu()?.f32s()?[offset])
261    }
262
263    /// The element at a logical index, as `f64` (from an `F64` tensor).
264    pub fn get_f64(&self, index: &[usize]) -> Result<f64> {
265        let offset = self.layout.offset_of(index)?;
266        Ok(self.storage.cpu()?.f64s()?[offset])
267    }
268
269    /// The element at a logical index, as `i64`.
270    pub fn get_i64(&self, index: &[usize]) -> Result<i64> {
271        let offset = self.layout.offset_of(index)?;
272        Ok(self.storage.cpu()?.i64s()?[offset])
273    }
274
275    /// Every element in logical (row-major) order, as `f32`, from CPU
276    /// storage.
277    pub fn to_vec_f32(&self) -> Result<Vec<f32>> {
278        let src = self.storage.cpu()?.f32s()?;
279        Ok(gather_logical(src, &self.layout))
280    }
281
282    /// Every element in logical (row-major) order, as `f64`, from an `F64`
283    /// CPU tensor (use [`Tensor::to_dtype`] first for an `f32` one).
284    pub fn to_vec_f64(&self) -> Result<Vec<f64>> {
285        let src = self.storage.cpu()?.f64s()?;
286        Ok(gather_logical(src, &self.layout))
287    }
288
289    /// Every element in logical (row-major) order, as `i64`.
290    pub fn to_vec_i64(&self) -> Result<Vec<i64>> {
291        let src = self.storage.cpu()?.i64s()?;
292        Ok(gather_logical(src, &self.layout))
293    }
294
295    // ---- views -----------------------------------------------------------
296
297    fn view(&self, layout: Layout) -> Self {
298        Self {
299            storage: Arc::clone(&self.storage),
300            layout,
301            autograd: None,
302        }
303    }
304
305    /// A view (or copy, when this view is not contiguous) with the same
306    /// elements in a new shape.
307    pub fn reshape(&self, shape: impl Into<Shape>) -> Result<Self> {
308        let shape = shape.into();
309        if checked_numel(&shape, "reshape")? != self.numel() {
310            return Err(Error::ShapeMismatch {
311                expected: self.shape().clone(),
312                got: shape,
313                op: "reshape",
314            });
315        }
316        let base = if self.layout.is_contiguous() {
317            self.clone()
318        } else {
319            self.contiguous_data()?
320        };
321        let layout = Layout {
322            strides: contiguous_strides(&shape),
323            shape,
324            offset: base.layout.offset,
325        };
326        let out = base.view(layout);
327        Ok(crate::ops::record_view(self, out, ViewKind::Reshape))
328    }
329
330    /// A view with dimensions reordered by `perm` (a permutation of
331    /// `0..ndim`).
332    pub fn permute(&self, perm: &[usize]) -> Result<Self> {
333        let n = self.ndim();
334        if perm.len() != n || {
335            let mut seen = vec![false; n];
336            perm.iter()
337                .any(|&p| p >= n || std::mem::replace(&mut seen[p], true))
338        } {
339            return Err(Error::InvalidArgument {
340                op: "permute",
341                detail: format!("{perm:?} is not a permutation of 0..{n}"),
342            });
343        }
344        let dims = self.dims();
345        let strides = self.layout.strides.values();
346        let new_dims: Vec<usize> = perm.iter().map(|&p| dims[p]).collect();
347        let new_strides: Vec<isize> = perm.iter().map(|&p| strides[p]).collect();
348        let layout = Layout {
349            shape: Shape::new(new_dims),
350            strides: Strides::new(new_strides),
351            offset: self.layout.offset,
352        };
353        let out = self.view(layout);
354        Ok(crate::ops::record_view(
355            self,
356            out,
357            ViewKind::Permute(perm.to_vec()),
358        ))
359    }
360
361    /// A view with dimensions `d0` and `d1` swapped.
362    pub fn transpose(&self, d0: usize, d1: usize) -> Result<Self> {
363        let mut perm: Vec<usize> = (0..self.ndim()).collect();
364        if d0 >= perm.len() || d1 >= perm.len() {
365            return Err(Error::InvalidArgument {
366                op: "transpose",
367                detail: format!("dims ({d0}, {d1}) out of range for rank {}", perm.len()),
368            });
369        }
370        perm.swap(d0, d1);
371        self.permute(&perm)
372    }
373
374    /// The matrix transpose: the last two dimensions swapped.
375    pub fn t(&self) -> Result<Self> {
376        let n = self.ndim();
377        if n < 2 {
378            return Err(Error::InvalidArgument {
379                op: "t",
380                detail: format!("needs rank >= 2, got {n}"),
381            });
382        }
383        self.transpose(n - 2, n - 1)
384    }
385
386    /// A view of `len` elements of dimension `dim` starting at `start`.
387    pub fn narrow(&self, dim: usize, start: usize, len: usize) -> Result<Self> {
388        let dims = self.dims();
389        let end = start.checked_add(len);
390        if dim >= dims.len() || end.is_none_or(|e| e > dims[dim]) {
391            return Err(Error::InvalidArgument {
392                op: "narrow",
393                detail: format!(
394                    "dim {dim}, start {start} len {len} against shape {:?}",
395                    self.shape()
396                ),
397            });
398        }
399        let mut new_dims = dims.to_vec();
400        new_dims[dim] = len;
401        let strides = self.layout.strides.values().to_vec();
402        let offset = (self.layout.offset as isize + start as isize * strides[dim]) as usize;
403        let layout = Layout {
404            shape: Shape::new(new_dims),
405            strides: Strides::new(strides),
406            offset,
407        };
408        let out = self.view(layout);
409        Ok(crate::ops::record_view(
410            self,
411            out,
412            ViewKind::Narrow { dim, start, len },
413        ))
414    }
415
416    /// A view of `range` along `dim` — sugar over [`Tensor::narrow`].
417    pub fn slice(&self, dim: usize, range: std::ops::Range<usize>) -> Result<Self> {
418        if range.end < range.start {
419            // Saturating to an empty view turned a caller mistake into a
420            // silently empty tensor that fails much later.
421            return Err(Error::InvalidArgument {
422                op: "slice",
423                detail: format!("range {}..{} is reversed", range.start, range.end),
424            });
425        }
426        self.narrow(dim, range.start, range.end - range.start)
427    }
428
429    /// A zero-copy broadcast view to `shape` (stride 0 on expanded axes).
430    pub fn broadcast_to(&self, shape: impl Into<Shape>) -> Result<Self> {
431        let shape = shape.into();
432        checked_numel(&shape, "broadcast_to")?;
433        let layout = broadcast_layout(&self.layout, &shape)?;
434        let out = self.view(layout);
435        Ok(crate::ops::record_view(self, out, ViewKind::Broadcast))
436    }
437
438    /// A broadcast view that records nothing on the tape — backend
439    /// plumbing; prefer [`Tensor::broadcast_to`] in user code.
440    pub fn broadcast_view(&self, shape: &Shape) -> Result<Self> {
441        checked_numel(shape, "broadcast_view")?;
442        let layout = broadcast_layout(&self.layout, shape)?;
443        Ok(self.view(layout))
444    }
445
446    /// A view with a new size-1 dimension inserted at `dim`.
447    pub fn unsqueeze(&self, dim: usize) -> Result<Self> {
448        let mut dims = self.dims().to_vec();
449        if dim > dims.len() {
450            return Err(Error::InvalidArgument {
451                op: "unsqueeze",
452                detail: format!("dim {dim} out of range for rank {}", dims.len()),
453            });
454        }
455        dims.insert(dim, 1);
456        let mut strides = self.layout.strides.values().to_vec();
457        strides.insert(dim, 0);
458        let layout = Layout {
459            shape: Shape::new(dims),
460            strides: Strides::new(strides),
461            offset: self.layout.offset,
462        };
463        let out = self.view(layout);
464        Ok(crate::ops::record_view(self, out, ViewKind::Reshape))
465    }
466
467    /// This tensor's elements, in logical order, in fresh contiguous
468    /// storage on the same device. A no-op clone when already contiguous.
469    pub fn contiguous(&self) -> Result<Self> {
470        if self.layout.is_contiguous() && self.layout.offset == 0 {
471            return Ok(self.clone());
472        }
473        let out = self.contiguous_data()?;
474        Ok(crate::ops::record_view(self, out, ViewKind::Contiguous))
475    }
476
477    /// The contiguous copy without autograd recording — backend plumbing;
478    /// prefer [`Tensor::contiguous`] in user code.
479    pub fn contiguous_untracked(&self) -> Result<Self> {
480        self.contiguous_data()
481    }
482
483    /// The contiguous copy without autograd recording (plumbing).
484    pub(crate) fn contiguous_data_crate(&self) -> Result<Self> {
485        self.contiguous_data()
486    }
487
488    /// The contiguous copy without autograd recording (plumbing).
489    pub(crate) fn contiguous_data(&self) -> Result<Self> {
490        match self.device() {
491            Device::Cpu => {
492                let shape = self.shape().clone();
493                match self.storage.cpu()? {
494                    CpuStorage::F32(_) => Tensor::from_vec_f32(self.to_vec_f32()?, shape),
495                    CpuStorage::F64(_) => Tensor::from_vec_f64(self.to_vec_f64()?, shape),
496                    CpuStorage::I64(_) => Tensor::from_vec_i64(self.to_vec_i64()?, shape),
497                    CpuStorage::U8(_) => Err(Error::UnsupportedDType {
498                        dtype: DType::U8,
499                        op: "contiguous",
500                    }),
501                }
502            }
503            device => backend_for(device)?.contiguous(self),
504        }
505    }
506
507    // ---- autograd surface -------------------------------------------------
508
509    /// Mark (or unmark) this tensor as a gradient-accumulating leaf, in
510    /// place, returning it for chaining.
511    pub fn requires_grad_(mut self, requires: bool) -> Self {
512        match (&self.autograd, requires) {
513            (Some(meta), _) if meta.grad_fn.is_none() => {
514                // Leaf: rebuild the meta with the new flag.
515                self.autograd = requires.then(|| {
516                    Arc::new(AutogradMeta {
517                        requires_grad: true,
518                        grad: Mutex::new(None),
519                        grad_fn: None,
520                    })
521                });
522            }
523            (Some(_), true) => { /* non-leaf already tracked; nothing to do */ }
524            (Some(_), false) => self.autograd = None,
525            (None, true) => {
526                self.autograd = Some(Arc::new(AutogradMeta {
527                    requires_grad: true,
528                    grad: Mutex::new(None),
529                    grad_fn: None,
530                }));
531            }
532            (None, false) => {}
533        }
534        self
535    }
536
537    /// Whether gradients **accumulate on this tensor** during `backward`
538    /// — true for leaves marked with [`requires_grad_`](Self::requires_grad_).
539    ///
540    /// This answers a narrower question than PyTorch's `requires_grad`:
541    /// a tensor *computed from* such a leaf is on the tape but does not
542    /// accumulate a gradient of its own, so it reports `false` here. Ask
543    /// [`is_tracked`](Self::is_tracked) for "is this on the graph at all".
544    pub fn requires_grad(&self) -> bool {
545        self.autograd.as_ref().is_some_and(|m| m.requires_grad)
546    }
547
548    /// Whether this tensor participates in the autograd tape at all —
549    /// true for a leaf that requires grad and for anything computed from
550    /// one while recording was enabled; false for constants and for
551    /// everything produced under [`no_grad`](crate::autograd::no_grad).
552    ///
553    /// This is the predicate that observes `no_grad`:
554    ///
555    /// ```
556    /// use oxmera_tensor::tensor::Tensor;
557    /// use oxmera_tensor::autograd::no_grad;
558    ///
559    /// let a = Tensor::from_slice(&[1.0, 2.0], [2]).unwrap().requires_grad_(true);
560    /// assert!(a.mul_scalar(3.0).unwrap().is_tracked());
561    /// assert!(!no_grad(|| a.mul_scalar(3.0).unwrap()).is_tracked());
562    /// ```
563    pub fn is_tracked(&self) -> bool {
564        self.autograd.is_some()
565    }
566
567    /// The accumulated gradient, if a backward pass has produced one.
568    pub fn grad(&self) -> Option<Tensor> {
569        self.autograd
570            .as_ref()
571            .and_then(|m| m.grad.lock().expect("grad mutex poisoned").clone())
572    }
573
574    /// Clear this tensor's accumulated gradient.
575    pub fn zero_grad(&self) {
576        if let Some(meta) = &self.autograd {
577            *meta.grad.lock().expect("grad mutex poisoned") = None;
578        }
579    }
580
581    /// The same view without any tape connection.
582    pub fn detach(&self) -> Self {
583        Self {
584            storage: Arc::clone(&self.storage),
585            layout: self.layout.clone(),
586            autograd: None,
587        }
588    }
589
590    /// Propagate gradients from this scalar through the recorded tape.
591    ///
592    /// Errors when the tensor is not a scalar; use
593    /// [`Tensor::backward_with`] to seed a non-scalar output.
594    pub fn backward(&self) -> Result<()> {
595        if self.numel() != 1 {
596            return Err(Error::InvalidArgument {
597                op: "backward",
598                detail: format!(
599                    "output has {} elements; seed a non-scalar with backward_with",
600                    self.numel()
601                ),
602            });
603        }
604        // The seed lives where the loss lives: a CPU seed against a
605        // device-resident graph failed at the first VJP with a
606        // DeviceMismatch (found by the CUDA backend's end-to-end test, and
607        // latent on Metal).
608        let seed = Tensor::ones(self.shape().clone())
609            .to_dtype(self.dtype())?
610            .to_device(self.device())?;
611        self.backward_with(seed)
612    }
613
614    /// Propagate gradients seeding this tensor's gradient with `seed`.
615    pub fn backward_with(&self, seed: Tensor) -> Result<()> {
616        let seed = if seed.device() == self.device() {
617            seed
618        } else {
619            seed.to_device(self.device())?
620        };
621        crate::autograd::run_backward(self, seed)
622    }
623
624    pub(crate) fn autograd_meta(&self) -> Option<Arc<AutogradMeta>> {
625        self.autograd.clone()
626    }
627
628    /// Attach a tape node to this tensor (used by the op layer).
629    /// Detach this tensor's tape node and hand it back, leaving the tensor a
630    /// leaf. `AutogradMeta::drop` uses this to dismantle a deep tape
631    /// iteratively rather than recursing once per node.
632    pub(crate) fn take_autograd(&mut self) -> Option<Arc<AutogradMeta>> {
633        self.autograd.take()
634    }
635
636    pub(crate) fn with_grad_fn(mut self, grad_fn: GradFn) -> Self {
637        self.autograd = Some(Arc::new(AutogradMeta {
638            requires_grad: false,
639            grad: Mutex::new(None),
640            grad_fn: Some(grad_fn),
641        }));
642        self
643    }
644}
645
646/// The internal view taxonomy the op layer uses to build view VJPs.
647#[derive(Debug, Clone)]
648pub(crate) enum ViewKind {
649    /// Reshape/unsqueeze: gradient reshapes back to the input shape.
650    Reshape,
651    /// Permute by this permutation: gradient permutes by the inverse.
652    Permute(Vec<usize>),
653    /// Narrow: gradient scatters back into zeros of the input shape.
654    Narrow {
655        /// Narrowed dimension.
656        dim: usize,
657        /// Range start.
658        start: usize,
659        /// Range length.
660        len: usize,
661    },
662    /// Broadcast view: gradient sum-reduces back to the input shape.
663    Broadcast,
664    /// Contiguous copy: gradient passes through (reshaped if needed).
665    Contiguous,
666}
667
668fn storage_len(storage: &Storage) -> usize {
669    match storage.data() {
670        crate::storage::StorageData::Cpu(c) => c.len(),
671        #[cfg(target_os = "macos")]
672        crate::storage::StorageData::Metal(b) => {
673            b.buffer().length() as usize / storage.dtype().size_in_bytes()
674        }
675        crate::storage::StorageData::Opaque(b) => b.len(),
676    }
677}
678
679/// The half-open range of storage indices a layout can address, as
680/// `(min, max_exclusive)`. Negative strides lower the minimum below the
681/// offset, so a valid layout needs `min >= 0` as well as
682/// `max_exclusive <= storage length`; the pre-0.5 check accounted for
683/// positive strides only and let an underflowing negative-stride layout
684/// through to a panic on the first read.
685fn addressed_bounds(layout: &Layout) -> Option<(isize, isize)> {
686    if layout.shape.checked_numel()? == 0 {
687        return Some((0, 0));
688    }
689    let base = isize::try_from(layout.offset).ok()?;
690    let mut lo = base;
691    let mut hi = base;
692    for (&d, &s) in layout.shape.dims().iter().zip(layout.strides.values()) {
693        if d > 1 {
694            let span = isize::try_from(d - 1).ok()?.checked_mul(s)?;
695            if s >= 0 {
696                hi = hi.checked_add(span)?;
697            } else {
698                lo = lo.checked_add(span)?;
699            }
700        }
701    }
702    Some((lo, hi.checked_add(1)?))
703}
704
705/// Broadcast `layout` to `target`, stride 0 on expanded axes.
706pub(crate) fn broadcast_layout(layout: &Layout, target: &Shape) -> Result<Layout> {
707    let src = layout.shape.dims();
708    let dst = target.dims();
709    if dst.len() < src.len() {
710        return Err(Error::BroadcastIncompatible {
711            lhs: layout.shape.clone(),
712            rhs: target.clone(),
713        });
714    }
715    let lead = dst.len() - src.len();
716    let mut strides = vec![0isize; dst.len()];
717    for i in 0..src.len() {
718        let (s, d) = (src[i], dst[lead + i]);
719        if s == d {
720            strides[lead + i] = layout.strides.values()[i];
721        } else if s == 1 {
722            strides[lead + i] = 0;
723        } else {
724            return Err(Error::BroadcastIncompatible {
725                lhs: layout.shape.clone(),
726                rhs: target.clone(),
727            });
728        }
729    }
730    Ok(Layout {
731        shape: target.clone(),
732        strides: Strides::new(strides),
733        offset: layout.offset,
734    })
735}
736
737/// Gather a strided view's elements into logical row-major order.
738pub(crate) fn gather_logical<T: Copy>(src: &[T], layout: &Layout) -> Vec<T> {
739    let numel = layout.shape.numel();
740    let mut out = Vec::with_capacity(numel);
741    if numel == 0 {
742        return out;
743    }
744    let dims = layout.shape.dims();
745    if layout.is_contiguous() {
746        let start = layout.offset;
747        out.extend_from_slice(&src[start..start + numel]);
748        return out;
749    }
750    let strides = layout.strides.values();
751    // Row fast path: when the innermost stride is 1 the row is a slice
752    // copy; when it is 0 (a broadcast dimension being materialized) the
753    // row is one value repeated. Only a genuinely strided innermost
754    // dimension — a transposed view — falls through to the odometer.
755    let ndim = dims.len();
756    let inner = dims[ndim - 1];
757    let inner_stride = strides[ndim - 1];
758    if inner > 0 && (inner_stride == 0 || inner_stride == 1) {
759        let outer = Layout {
760            shape: Shape::new(dims[..ndim - 1].to_vec()),
761            strides: Strides::new(strides[..ndim - 1].to_vec()),
762            offset: layout.offset,
763        };
764        let mut walker = crate::cpu_iter::OffsetWalker::at(&outer, 0);
765        for _ in 0..numel / inner {
766            let base = walker.next_offset();
767            if inner_stride == 1 {
768                out.extend_from_slice(&src[base..base + inner]);
769            } else {
770                out.resize(out.len() + inner, src[base]);
771            }
772        }
773        return out;
774    }
775    let mut index = vec![0usize; dims.len()];
776    let mut offset = layout.offset as isize;
777    loop {
778        out.push(src[offset as usize]);
779        // Odometer increment, last dimension fastest, offset updated
780        // incrementally.
781        let mut d = dims.len();
782        loop {
783            if d == 0 {
784                return out;
785            }
786            d -= 1;
787            index[d] += 1;
788            offset += strides[d];
789            if index[d] < dims[d] {
790                break;
791            }
792            offset -= dims[d] as isize * strides[d];
793            index[d] = 0;
794        }
795        if out.len() == numel {
796            return out;
797        }
798    }
799}
800
801/// The element count of a caller-supplied shape, or a typed error when it
802/// does not fit in `usize` (see [`Shape::checked_numel`]).
803/// `numel` copies of `value`, reporting an allocation failure as a typed
804/// error. `vec![value; numel]` *aborts the process* when the allocator
805/// refuses, which a `try_` constructor must never do — that is the whole
806/// reason the caller reached for the fallible form.
807fn try_filled(numel: usize, value: f32, op: &'static str) -> Result<Vec<f32>> {
808    let mut data: Vec<f32> = Vec::new();
809    data.try_reserve_exact(numel)
810        .map_err(|_| Error::InvalidArgument {
811            op,
812            detail: format!("cannot allocate {numel} f32 elements"),
813        })?;
814    data.resize(numel, value);
815    Ok(data)
816}
817
818fn checked_numel(shape: &Shape, op: &'static str) -> Result<usize> {
819    shape.checked_numel().ok_or_else(|| Error::InvalidArgument {
820        op,
821        detail: format!(
822            "shape {:?} has more elements than fit in usize",
823            shape.dims()
824        ),
825    })
826}