Skip to main content

burn_std/tensor/
layout.rs

1//! The order a tensor's dimensions occupy memory in.
2//!
3//! A tensor's strides say which dimension is innermost, which next, and so on
4//! outward — its *dimension order*. Contiguous NCHW is the identity order; a
5//! convolution that computes channels-last and hands back a permuted view is
6//! `[0, 2, 3, 1]`. A kernel that iterates a tensor in its own dimension order
7//! reads it linearly; one that iterates in any other order reads it strided.
8//!
9//! Two questions are asked here. [dim_order] asks for the order of a tensor
10//! that is *dense* — that fills its buffer with no gaps — which is what a kernel
11//! needs before it may treat the buffer as a flat run of elements.
12//! [nested_dim_order] asks only that the dimensions *nest*, tolerating the gap a
13//! pitched or tile-aligned allocation leaves under a dimension, which is enough
14//! to decide what order to iterate in.
15
16use alloc::vec::Vec;
17
18use crate::Shape;
19
20/// The order a tensor's dimensions appear in memory, outermost first.
21///
22/// `[0, 1, 2, 3]` is contiguous NCHW; `[0, 2, 3, 1]` is NHWC. This is the same
23/// convention as the permutation passed to `Tensor::permute`.
24pub type DimOrder = Shape;
25
26/// The dimension order of a tensor that is dense in memory, or `None` if it is
27/// not dense.
28///
29/// Dense means the strides are exactly a permutation of contiguous strides: no
30/// gaps, no overlap, no broadcasting. Use [nested_dim_order] when only an
31/// iteration order is needed and gaps in storage are acceptable.
32///
33/// Dimensions of size one are ignored while checking density — their stride is
34/// arbitrary and carries no traffic — but they keep a position in the returned
35/// order so it stays a permutation of `0..rank`.
36pub fn dim_order(shape: &[usize], strides: &[usize]) -> Option<DimOrder> {
37    dim_order_inner(shape, strides, Padding::Rejected)
38}
39
40/// The dimension order of a tensor whose dimensions nest without overlapping,
41/// or `None` if they do not.
42///
43/// Weaker than [dim_order], which additionally requires consecutive elements
44/// without gaps. A dimension may sit at a larger stride than the extents inside it
45/// need, which is what a pitched or tile-aligned allocation produces: 48
46/// channels held innermost on a 64-element tile have stride 64 where a dense
47/// tensor would have 48.
48///
49/// Iterating in this order can improve locality, but reads must still use the
50/// tensor's actual strides. Gaps can affect memory transactions and vectorization;
51/// accepting an order does not guarantee dense-access performance. This also
52/// accepts sliced views whose dimensions satisfy the same nesting condition.
53/// Anything reinterpreting a buffer as a flat run of elements must keep asking
54/// [dim_order].
55pub fn nested_dim_order(shape: &[usize], strides: &[usize]) -> Option<DimOrder> {
56    dim_order_inner(shape, strides, Padding::Allowed)
57}
58
59#[derive(Clone, Copy)]
60enum Padding {
61    Allowed,
62    Rejected,
63}
64
65fn dim_order_inner(shape: &[usize], strides: &[usize], padding: Padding) -> Option<DimOrder> {
66    let rank = shape.len();
67
68    if rank != strides.len() {
69        return None;
70    }
71
72    let mut order: Vec<usize> = (0..rank).collect();
73    // Descending stride is outermost first. The dimension index breaks ties so
74    // that equal strides — which only happens among size-one dimensions — give
75    // a deterministic order rather than one that depends on the sort.
76    order.sort_by(|a, b| strides[*b].cmp(&strides[*a]).then(a.cmp(b)));
77
78    let mut expected = 1;
79
80    for &axis in order.iter().rev() {
81        if shape[axis] == 1 {
82            continue;
83        }
84        match padding {
85            // A gap is what makes the tensor padded rather than dense; an
86            // overlap is not a layout at all.
87            Padding::Allowed if strides[axis] < expected => return None,
88            Padding::Rejected if strides[axis] != expected => return None,
89            _ => {}
90        }
91        // Where the stride is exactly `expected` this is `expected *= shape[axis]`,
92        // so the dense walk is the padded one with the gaps taken out.
93        expected = strides[axis] * shape[axis];
94    }
95
96    Some(Shape::from(order))
97}
98
99/// Whether a dimension order is the contiguous one, `[0, 1, .., rank - 1]`.
100pub fn is_contiguous_order(order: &[usize]) -> bool {
101    order.iter().enumerate().all(|(pos, axis)| pos == *axis)
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use alloc::vec;
108
109    #[test]
110    fn contiguous_is_the_identity_order() {
111        let shape = [2, 48, 16, 16];
112        let strides = [48 * 16 * 16, 16 * 16, 16, 1];
113
114        assert_eq!(
115            dim_order(&shape, &strides),
116            Some(Shape::from(vec![0, 1, 2, 3]))
117        );
118    }
119
120    #[test]
121    fn nhwc_memory_gives_the_nhwc_order() {
122        // What a convolution hands its successor: shape is NCHW, memory is NHWC.
123        let shape = [2, 48, 16, 16];
124        let strides = [16 * 16 * 48, 1, 16 * 48, 48];
125
126        assert_eq!(
127            dim_order(&shape, &strides),
128            Some(Shape::from(vec![0, 2, 3, 1]))
129        );
130    }
131
132    #[test]
133    fn broadcast_is_not_dense() {
134        let shape = [2, 48, 16, 16];
135        let strides = [0, 1, 0, 0];
136
137        assert_eq!(dim_order(&shape, &strides), None);
138    }
139
140    #[test]
141    fn a_slice_is_not_dense() {
142        // A view into a wider tensor: the row stride overshoots the row.
143        let shape = [4, 8];
144        let strides = [16, 1];
145
146        assert_eq!(dim_order(&shape, &strides), None);
147    }
148
149    #[test]
150    fn size_one_dimensions_do_not_decide_density() {
151        // A per-channel parameter presented at full rank. The strides of the
152        // degenerate dimensions say nothing, and must not make it non-dense.
153        let shape = [1, 48, 1, 1];
154        let strides = [48, 1, 48, 48];
155
156        assert!(dim_order(&shape, &strides).is_some());
157    }
158
159    #[test]
160    fn the_order_ends_at_the_innermost_dimension() {
161        let shape = [2, 48, 16, 16];
162
163        let contiguous = dim_order(&shape, &[48 * 16 * 16, 16 * 16, 16, 1]).unwrap();
164        let nhwc = dim_order(&shape, &[16 * 16 * 48, 1, 16 * 48, 48]).unwrap();
165
166        assert_eq!(contiguous.last(), Some(&3));
167        assert_eq!(nhwc.last(), Some(&1));
168    }
169
170    #[test]
171    fn order_is_a_permutation() {
172        assert!(is_contiguous_order(&[0, 1, 2, 3]));
173        assert!(!is_contiguous_order(&[0, 2, 3, 1]));
174    }
175
176    #[test]
177    fn a_dense_tensor_nests_in_the_order_it_is_dense_in() {
178        let shape = [2, 48, 16, 16];
179
180        for strides in [
181            [48 * 16 * 16, 16 * 16, 16, 1],
182            [16 * 16 * 48, 1, 16 * 48, 48],
183        ] {
184            let dense = dim_order(&shape, &strides);
185            assert!(dense.is_some());
186            assert_eq!(nested_dim_order(&shape, &strides), dense);
187        }
188    }
189
190    #[test]
191    fn padding_under_the_innermost_dimension_keeps_the_nhwc_order() {
192        let shape = [2, 48, 16, 16];
193        let strides = [16 * 16 * 64, 1, 16 * 64, 64];
194
195        assert_eq!(dim_order(&shape, &strides), None);
196        assert_eq!(
197            nested_dim_order(&shape, &strides),
198            Some(Shape::from(vec![0, 2, 3, 1]))
199        );
200    }
201
202    #[test]
203    fn padding_above_the_innermost_dimension_is_nesting_too() {
204        let shape = [4, 8];
205        let strides = [16, 1];
206
207        assert_eq!(dim_order(&shape, &strides), None);
208        assert_eq!(
209            nested_dim_order(&shape, &strides),
210            Some(Shape::from(vec![0, 1]))
211        );
212    }
213
214    #[test]
215    fn overlapping_dimensions_are_not_an_order_under_either() {
216        let shape = [4, 8];
217        let strides = [4, 1];
218
219        assert_eq!(dim_order(&shape, &strides), None);
220        assert_eq!(nested_dim_order(&shape, &strides), None);
221    }
222
223    #[test]
224    fn a_broadcast_dimension_still_cannot_vote() {
225        let shape = [2, 48, 16, 16];
226        let strides = [0, 1, 0, 0];
227
228        assert_eq!(nested_dim_order(&shape, &strides), None);
229    }
230
231    #[test]
232    fn size_one_dimensions_neither_pad_nor_constrain_nesting() {
233        let shape = [1, 48, 1, 1];
234        let strides = [48, 1, 48, 48];
235
236        assert_eq!(
237            nested_dim_order(&shape, &strides),
238            dim_order(&shape, &strides)
239        );
240        assert!(nested_dim_order(&shape, &strides).is_some());
241    }
242}