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}