Skip to main content

candela/tensor/mem_formats/
layout.rs

1use std::{hash::Hash, iter::zip};
2
3use crate::tensor::{
4    errors::OpError,
5    internals::{calculate_adjacent_dim_stride, calculate_dim_stride},
6    mem_formats::slice::{SliceInfo, SliceRange},
7};
8
9/// How a tensor's logical shape maps onto its flat backing buffer.
10///
11/// A `Layout` bundles the `shape`, the per-axis `stride` (how many buffer
12/// elements to step to advance one index along that axis), an `offset` into the
13/// buffer, and a cached total `len`. Views, slices, transposes, and broadcasts
14/// are all just new layouts over the *same* buffer, which is what makes those
15/// operations zero-copy. See the [layout docs](crate::docs::layout) for the full
16/// model, including the `adj_stride` iteration trick.
17///
18/// You rarely build one by hand: a tensor's layout fields are reachable directly
19/// through the [`Dimension`](crate::Dimension) trait (`t.shape()`, `t.stride()`,
20/// `t.is_contiguous()`, …), and `t.layout()` hands back the whole `Layout`.
21/// Constructing one explicitly is mostly useful for shaping a skeleton slot.
22///
23/// # Examples
24///
25/// ```
26/// use candela::{Dimension, Layout, Tensor};
27///
28/// // Shape/stride are available straight off the tensor via `Dimension`.
29/// let t = Tensor::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
30/// assert_eq!(t.shape(), &[2, 3]);
31/// assert_eq!(t.layout(), &Layout::new(&[2, 3]));
32///
33/// // Or build one directly.
34/// let l = Layout::new(&[2, 3]);
35/// assert_eq!(l.stride(), &[3, 1]);
36/// assert!(l.is_contiguous());
37/// ```
38#[derive(Clone, Debug)]
39pub struct Layout {
40    pub(crate) shape: Box<[usize]>,
41    pub(crate) stride: Box<[i32]>,
42    pub(crate) adj_stride: Box<[i32]>,
43    pub(crate) offset: usize,
44    pub(crate) len: usize,
45}
46
47#[inline]
48pub(crate) fn validate_shape(shape: &[usize]) -> Result<(), OpError> {
49    if shape.is_empty() {
50        return Err(OpError::ZeroRankShape);
51    }
52    Ok(())
53}
54
55impl Layout {
56    /// Build a contiguous, row-major layout for `shape`.
57    ///
58    /// # Panics
59    ///
60    /// Panics if `shape` is empty (a tensor must have rank >= 1).
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use candela::Layout;
66    /// let l = Layout::new(&[2, 3]);
67    /// assert_eq!(l.shape(), &[2, 3]);
68    /// assert_eq!(l.stride(), &[3, 1]);
69    /// assert_eq!(l.len(), 6);
70    /// ```
71    pub fn new(shape: &[usize]) -> Self {
72        validate_shape(shape).unwrap_or_else(|e| panic!("{}", e));
73        let len: usize = shape.iter().product();
74
75        Self {
76            shape: shape.into(),
77            stride: calculate_dim_stride(shape),
78            adj_stride: vec![1; shape.len()].into_boxed_slice(),
79            offset: 0,
80            len,
81        }
82    }
83
84    /// Build the empty layout: shape `[0]`, length `0`.
85    ///
86    /// # Examples
87    ///
88    /// ```
89    /// use candela::Layout;
90    /// assert!(Layout::empty().is_empty());
91    /// ```
92    pub fn empty() -> Self {
93        Self {
94            shape: Box::new([0]),
95            stride: Box::new([0]),
96            adj_stride: Box::new([0]),
97            offset: 0,
98            len: 0,
99        }
100    }
101
102    /// Assemble a layout from already-computed fields, without validation.
103    ///
104    /// An escape hatch for callers that have already worked out every field,
105    /// including the `adj_stride` iteration helper. Prefer [`new`](Self::new) or
106    /// [`from_strided`](Self::from_strided), which derive those for you.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use candela::Layout;
112    /// // A hand-built 2x3 contiguous layout, equivalent to `Layout::new(&[2, 3])`.
113    /// let l = Layout::from_raw_parts(
114    ///     Box::from([2, 3]),
115    ///     Box::from([3, 1]),
116    ///     Box::from([1, 1]),
117    ///     0,
118    ///     6,
119    /// );
120    /// assert_eq!(l.shape(), &[2, 3]);
121    /// assert_eq!(l.len(), 6);
122    /// ```
123    pub fn from_raw_parts(
124        shape: Box<[usize]>,
125        stride: Box<[i32]>,
126        adj_stride: Box<[i32]>,
127        offset: usize,
128        len: usize,
129    ) -> Self {
130        Self {
131            shape,
132            stride,
133            adj_stride,
134            offset,
135            len,
136        }
137    }
138
139    /// Build a layout for `shape` with an explicit `stride` and `offset`.
140    ///
141    /// The `adj_stride` iteration helper is derived for you. Use this to describe
142    /// non-contiguous data - a stride of `0` on an axis, for instance, repeats
143    /// that axis (broadcasting).
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// use candela::Layout;
149    /// // Column-major 2x3: advancing a column steps 1, advancing a row steps 2.
150    /// let l = Layout::from_strided(&[2, 3], &[1, 2], 0);
151    /// assert_eq!(l.shape(), &[2, 3]);
152    /// assert_eq!(l.stride(), &[1, 2]);
153    /// ```
154    pub fn from_strided(shape: &[usize], stride: &[i32], offset: usize) -> Self {
155        validate_shape(shape).unwrap_or_else(|e| panic!("{}", e));
156        debug_assert!(shape.len() == stride.len());
157
158        let len: usize = shape.iter().product();
159
160        Self {
161            shape: shape.into(),
162            stride: stride.into(),
163            adj_stride: calculate_adjacent_dim_stride(stride, shape),
164            offset,
165            len,
166        }
167    }
168
169    /// Return this layout with its `offset` into the backing buffer replaced.
170    ///
171    /// Builder-style, usually chained onto [`new`](Self::new).
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use candela::Layout;
177    /// let l = Layout::new(&[4]).with_offset(3);
178    /// assert_eq!(l.offset(), 3);
179    /// ```
180    pub fn with_offset(mut self, offset: usize) -> Self {
181        self.offset = offset;
182
183        self
184    }
185
186    /// Derive a layout with a new `shape` but the same element count.
187    ///
188    /// # Errors
189    ///
190    /// Returns [`OpError::InvalidViewShape`] if `shape`'s element count differs
191    /// from this layout's, or [`OpError::NonContiguousView`] if this layout is
192    /// not contiguous (viewing needs a contiguous source).
193    ///
194    /// # Examples
195    ///
196    /// ```
197    /// use candela::Layout;
198    /// let l = Layout::new(&[2, 3]);
199    /// assert_eq!(l.view(&[3, 2])?.shape(), &[3, 2]);
200    /// assert!(l.view(&[4, 4]).is_err()); // 16 != 6 elements
201    /// # Ok::<(), candela::OpError>(())
202    /// ```
203    pub fn view(&self, shape: &[usize]) -> Result<Self, OpError> {
204        if shape.iter().product::<usize>() != self.len() {
205            return Err(OpError::InvalidViewShape);
206        }
207        if !self.is_contiguous() {
208            return Err(OpError::NonContiguousView);
209        }
210        Ok(Layout::new(shape).with_offset(self.offset))
211    }
212
213    /// Derive the layout of a sub-region, one [`SliceRange`] per leading axis.
214    ///
215    /// Axes without a range are taken in full. Build the range list with the
216    /// [`s!`](crate::s) macro.
217    ///
218    /// # Errors
219    ///
220    /// Returns [`OpError::AxesOutOfBounds`] if `range` has more entries than the
221    /// layout has axes, or a slice error if a range is empty or runs past its axis.
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// use candela::{s, Layout};
227    /// let l = Layout::new(&[2, 3]);
228    /// let sub = l.slice(s![0..1, 1..3])?;
229    /// assert_eq!(sub.shape(), &[1, 2]);
230    /// # Ok::<(), candela::OpError>(())
231    /// ```
232    pub fn slice(&self, range: &[SliceRange]) -> Result<Self, OpError> {
233        let info = SliceInfo::from_range(self, range)?;
234        let len: usize = info.shape.iter().product();
235
236        Ok(Self {
237            shape: info.shape,
238            stride: self.stride.clone(),
239            adj_stride: info.adj_stride,
240            offset: info.offset,
241            len,
242        })
243    }
244
245    /// Reverse the order of every axis (a full transpose), swapping both the
246    /// shape and the stride end for end.
247    ///
248    /// # Examples
249    ///
250    /// ```
251    /// use candela::Layout;
252    /// let t = Layout::new(&[2, 3]).transpose();
253    /// assert_eq!(t.shape(), &[3, 2]);
254    /// assert_eq!(t.stride(), &[1, 3]);
255    /// ```
256    pub fn transpose(&self) -> Self {
257        let mut stride = self.stride.clone();
258        let mut shape = self.shape.clone();
259
260        for i in 0..stride.len() / 2 {
261            let last = stride.len() - i - 1;
262
263            let temp = stride[last];
264            stride[last] = stride[i];
265            stride[i] = temp;
266
267            let temp = shape[last];
268            shape[last] = shape[i];
269            shape[i] = temp;
270        }
271
272        let adj_stride: Box<[i32]> = calculate_adjacent_dim_stride(&stride, &shape);
273
274        Self {
275            shape,
276            stride,
277            adj_stride,
278            offset: self.offset,
279            len: self.len,
280        }
281    }
282
283    /// Reorders the axes by an explicit permutation.
284    ///
285    /// `axes` must list every axis index exactly once: `transpose_axes(&[1, 0])`
286    /// is the plain 2-D [`.transpose()`][Self::transpose], while `&[0, 2, 1]`
287    /// swaps only the last two axes of a rank-3 layout and leaves the first alone.
288    ///
289    /// # Examples
290    ///
291    /// ```
292    /// use candela::Layout;
293    ///
294    /// let l = Layout::new(&[1, 2, 3]);
295    /// let s = l.transpose_axes(&[0, 2, 1])?;
296    /// assert_eq!(s.shape(), &[1, 3, 2]);
297    /// # Ok::<(), candela::OpError>(())
298    /// ```
299    ///
300    /// # Errors
301    ///
302    /// Returns [`OpError::NotEnoughAxes`] if `axes` doesn't have one entry per
303    /// axis, or [`OpError::AxesOutOfBounds`] if an index is out of range or
304    /// repeated (so the list isn't a valid permutation).
305    pub fn transpose_axes(&self, axes: &[usize]) -> Result<Self, OpError> {
306        if axes.len() != self.stride.len() {
307            return Err(OpError::NotEnoughAxes(self.stride.len(), axes.len()));
308        }
309
310        for (i, axis) in axes.iter().enumerate() {
311            for axis_other in axes.iter().skip(i + 1) {
312                if axis == axis_other {
313                    return Err(OpError::AxesOutOfBounds);
314                }
315            }
316        }
317
318        let mut stride: Vec<i32> = Vec::with_capacity(self.stride.len());
319        let mut shape: Vec<usize> = Vec::with_capacity(self.stride.len());
320
321        for &axis in axes.iter() {
322            if axis >= self.stride.len() {
323                return Err(OpError::AxesOutOfBounds);
324            }
325
326            stride.push(self.stride[axis]);
327            shape.push(self.shape[axis]);
328        }
329
330        let adj_stride = calculate_adjacent_dim_stride(&stride, &shape);
331
332        Ok(Self {
333            shape: shape.into_boxed_slice(),
334            stride: stride.into_boxed_slice(),
335            adj_stride,
336            offset: self.offset,
337            len: self.len,
338        })
339    }
340
341    /// Expands the layout to a larger shape along new or size-1 axes.
342    ///
343    /// Broadcasting follows NumPy's right-aligned rules: a target axis must
344    /// either match the source or expand from size 1, and extra leading axes are
345    /// added on the left. The repeated axes are faked with zero strides.
346    ///
347    /// # Examples
348    ///
349    /// ```
350    /// use candela::Layout;
351    ///
352    /// let row = Layout::new(&[1, 3]);
353    /// let b = row.broadcast(&[2, 3])?;
354    /// assert_eq!(b.shape(), &[2, 3]);
355    /// # Ok::<(), candela::OpError>(())
356    /// ```
357    ///
358    /// # Errors
359    ///
360    /// Returns [`OpError::CannotBroadcast`] if the target shape has fewer axes
361    /// than the source, or an axis is neither equal to the source nor expandable
362    /// from 1.
363    pub fn broadcast(&self, shape: &[usize]) -> Result<Self, OpError> {
364        if shape.len() < self.shape.len() {
365            return Err(OpError::CannotBroadcast);
366        }
367
368        for (s1, s2) in zip(shape.iter().rev(), self.shape.iter().rev()) {
369            if *s2 != 1 && *s1 != *s2 {
370                return Err(OpError::CannotBroadcast);
371            }
372        }
373
374        let mut new_stride: Vec<i32> = Vec::with_capacity(shape.len());
375        new_stride.extend(
376            (self.shape.len()..shape.len())
377                .map(|_| 0)
378                .chain(self.stride.iter().cloned()),
379        );
380
381        let len = new_stride.len();
382
383        for (dim, s) in self.shape.iter().rev().enumerate() {
384            if *s == 1 {
385                new_stride[len - dim - 1] = 0;
386            }
387        }
388
389        let adj_stride = calculate_adjacent_dim_stride(&new_stride, shape);
390        let len: usize = shape.iter().product();
391
392        Ok(Self {
393            shape: shape.into(),
394            stride: new_stride.into_boxed_slice(),
395            adj_stride,
396            offset: self.offset,
397            len,
398        })
399    }
400
401    /// Collapses the shape into a canonical `[batch, rows, cols]` triple.
402    ///
403    /// The last two axes become `rows` and `cols`; everything above them is
404    /// folded into a single `batch` count, and ranks below 3 are padded with
405    /// leading ones. The matmul kernel uses this to treat any rank uniformly.
406    ///
407    /// # Examples
408    ///
409    /// ```
410    /// use candela::Layout;
411    /// assert_eq!(Layout::new(&[5]).shape_as_3d(), [1, 1, 5]);
412    /// assert_eq!(Layout::new(&[2, 3]).shape_as_3d(), [1, 2, 3]);
413    /// assert_eq!(Layout::new(&[2, 3, 4]).shape_as_3d(), [2, 3, 4]);
414    /// assert_eq!(Layout::new(&[6, 2, 3, 4]).shape_as_3d(), [12, 3, 4]);
415    /// ```
416    #[inline]
417    pub fn shape_as_3d(&self) -> [usize; 3] {
418        debug_assert!(!self.shape.is_empty(), "shape_as_3d requires rank >= 1");
419        if self.shape.len() == 1 {
420            [1, 1, self.shape[0]]
421        } else if self.shape.len() == 2 {
422            [1, self.shape[0], self.shape[1]]
423        } else {
424            let len = self.shape.len();
425
426            let mut acc: usize = 1;
427            for i in 0..len - 2 {
428                acc *= self.shape[i];
429            }
430
431            [acc, self.shape[len - 2], self.shape[len - 1]]
432        }
433    }
434
435    /// Cyclically rotates the axes so that iterating the layout walks along
436    /// `axis`: `axis` becomes the innermost (fastest-varying) dimension, and
437    /// every axis above it (`0..=axis`) is pulled inward as the surrounding
438    /// block, so a flat iterator steps through all of their index combinations
439    /// before advancing the trailing axes. The trailing axes (`axis+1..`) stay
440    /// outermost in their original order.
441    ///
442    /// # Examples
443    ///
444    /// ```
445    /// use candela::Layout;
446    /// let r = Layout::new(&[2, 3, 4]).rotate_axis_innermost(0)?;
447    /// assert_eq!(r.shape(), &[3, 4, 2]);
448    /// # Ok::<(), candela::OpError>(())
449    /// ```
450    ///
451    /// # Errors
452    ///
453    /// Returns [`OpError::AxesOutOfBounds`] if `axis` is past the layout's rank.
454    #[inline]
455    pub fn rotate_axis_innermost(&self, axis: usize) -> Result<Self, OpError> {
456        if axis >= self.shape().len() {
457            return Err(OpError::AxesOutOfBounds);
458        }
459
460        let mut axes: Vec<usize> = (axis + 1..self.shape().len()).collect();
461        axes.extend(0..=axis);
462
463        unsafe { Ok(self.transpose_axes(&axes).unwrap_unchecked()) }
464    }
465
466    /// Returns `true` if a flat walk visits every element in row-major order with
467    /// no gaps - i.e. the layout has not been transposed, broadcast, or sliced
468    /// down the inner axes.
469    ///
470    /// # Examples
471    ///
472    /// ```
473    /// use candela::Layout;
474    /// assert!(Layout::new(&[3, 4]).is_contiguous());
475    /// assert!(!Layout::new(&[3, 4]).transpose().is_contiguous());
476    /// ```
477    #[inline]
478    pub fn is_contiguous(&self) -> bool {
479        self.is_contiguous_at_axis(0)
480    }
481
482    /// Like [`is_contiguous`](Self::is_contiguous), but only checks the axes from
483    /// `axis` inward, ignoring how the outer axes are arranged.
484    ///
485    /// An out-of-range `axis` returns `false`.
486    ///
487    /// # Examples
488    ///
489    /// ```
490    /// use candela::Layout;
491    /// let l = Layout::new(&[2, 3]);
492    /// assert!(l.is_contiguous_at_axis(0));
493    /// assert!(!l.is_contiguous_at_axis(9)); // out of range
494    /// ```
495    #[inline]
496    pub fn is_contiguous_at_axis(&self, axis: usize) -> bool {
497        if axis >= self.shape().len() {
498            return false;
499        }
500
501        self.adj_stride[axis] == 1 && !self.stride[axis + 1..].contains(&0)
502    }
503
504    /// Returns `true` if any axis runs backwards relative to a contiguous layout -
505    /// the signature a transpose leaves behind. Broadcast axes (zero stride) are
506    /// not counted.
507    ///
508    /// # Examples
509    ///
510    /// ```
511    /// use candela::Layout;
512    /// assert!(!Layout::new(&[3, 4]).is_transposed());
513    /// assert!(Layout::new(&[3, 4]).transpose().is_transposed());
514    /// ```
515    #[inline]
516    pub fn is_transposed(&self) -> bool {
517        for (i, &adj_stride) in self.adj_stride.iter().enumerate() {
518            if adj_stride < 0 && self.stride[i] != 0 {
519                return true;
520            }
521        }
522
523        false
524    }
525
526    /// Like [`is_transposed`](Self::is_transposed), but tests a single `axis`.
527    ///
528    /// An out-of-range `axis` returns `false`.
529    ///
530    /// # Examples
531    ///
532    /// ```
533    /// use candela::Layout;
534    /// let l = Layout::new(&[3, 4]); // fresh: no axis is transposed
535    /// assert!(!l.is_transposed_at_axis(0));
536    /// assert!(!l.is_transposed_at_axis(9)); // out of range
537    /// ```
538    #[inline]
539    pub fn is_transposed_at_axis(&self, axis: usize) -> bool {
540        if axis >= self.shape().len() {
541            return false;
542        }
543
544        self.adj_stride[axis] < 0 && self.stride[axis] != 0
545    }
546
547    /// Returns `true` for a 2-D layout whose two axes are transposed; always
548    /// `false` for any other rank.
549    ///
550    /// # Examples
551    ///
552    /// ```
553    /// use candela::Layout;
554    /// assert!(Layout::new(&[3, 4]).transpose().is_last_axes_transposed());
555    /// assert!(!Layout::new(&[3, 4]).is_last_axes_transposed());
556    /// ```
557    // Restricted to 2D on purpose: the matmul kernel uses this to pick the BLAS
558    // trans-flag, and its batch-stride handling assumes there is no batch dim.
559    // Higher-rank tensors whose last two strides happen to match this pattern
560    // would silently feed an incoherent batch stride to GEMM.
561    #[inline]
562    pub fn is_last_axes_transposed(&self) -> bool {
563        if self.shape.len() != 2 {
564            return false;
565        }
566
567        let rs = self.stride[self.stride.len() - 2];
568        let cs = self.stride[self.stride.len() - 1];
569
570        // Gives false on broadcasting
571        if rs == 0 || cs == 0 {
572            return false;
573        }
574
575        // cs must be > 1: a contiguous [m, 1] matrix has rs=cs=1 and is not transposed
576        rs == 1 && cs > 1
577    }
578
579    /// The size of each axis.
580    ///
581    /// # Examples
582    ///
583    /// ```
584    /// use candela::Layout;
585    /// assert_eq!(Layout::new(&[2, 3]).shape(), &[2, 3]);
586    /// ```
587    #[inline]
588    pub fn shape(&self) -> &'_ [usize] {
589        &self.shape
590    }
591
592    /// The per-axis stride: how many buffer elements to step to advance one
593    /// index along that axis.
594    ///
595    /// # Examples
596    ///
597    /// ```
598    /// use candela::Layout;
599    /// assert_eq!(Layout::new(&[2, 3]).stride(), &[3, 1]);
600    /// ```
601    #[inline]
602    pub fn stride(&self) -> &'_ [i32] {
603        &self.stride
604    }
605
606    /// The adjacent stride: the per-axis step a flat iterator applies when it
607    /// rolls over into the next axis. See the [layout docs](crate::docs::layout).
608    ///
609    /// # Examples
610    ///
611    /// ```
612    /// use candela::Layout;
613    /// // All ones for a freshly built contiguous layout.
614    /// assert_eq!(Layout::new(&[2, 3]).adj_stride(), &[1, 1]);
615    /// ```
616    #[inline]
617    pub fn adj_stride(&self) -> &'_ [i32] {
618        &self.adj_stride
619    }
620
621    /// The starting index into the backing buffer.
622    ///
623    /// # Examples
624    ///
625    /// ```
626    /// use candela::Layout;
627    /// assert_eq!(Layout::new(&[4]).with_offset(3).offset(), 3);
628    /// ```
629    #[inline]
630    pub fn offset(&self) -> usize {
631        self.offset
632    }
633
634    /// The total number of elements, i.e. the product of the shape.
635    ///
636    /// # Examples
637    ///
638    /// ```
639    /// use candela::Layout;
640    /// assert_eq!(Layout::new(&[2, 3, 4]).len(), 24);
641    /// ```
642    #[inline]
643    pub fn len(&self) -> usize {
644        self.len
645    }
646
647    /// Returns `true` if the layout has no elements.
648    ///
649    /// # Examples
650    ///
651    /// ```
652    /// use candela::Layout;
653    /// assert!(Layout::empty().is_empty());
654    /// assert!(!Layout::new(&[3]).is_empty());
655    /// ```
656    #[inline]
657    pub fn is_empty(&self) -> bool {
658        self.len == 0
659    }
660}
661
662impl PartialEq for Layout {
663    fn eq(&self, other: &Self) -> bool {
664        self.shape == other.shape && self.stride == other.stride
665    }
666}
667
668impl Eq for Layout {}
669
670impl Hash for Layout {
671    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
672        self.shape.hash(state);
673        self.stride.hash(state);
674    }
675}
676
677#[cfg(test)]
678#[path = "layout_tests.rs"]
679mod tests;
680
681impl std::fmt::Display for Layout {
682    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
683        write!(
684            f,
685            "Layout {{ shape: {:?}, stride: {:?}, offset: {} }}",
686            &self.shape, &self.stride, self.offset
687        )
688    }
689}