Skip to main content

hanzo_ml/
layout.rs

1//! Tensor Layouts including contiguous or sparse strides
2
3use crate::{Error, Result, Shape};
4
5#[derive(Debug, PartialEq, Eq, Clone)]
6pub struct Layout {
7    shape: Shape,
8    // The strides are given in number of elements and not in bytes.
9    stride: Vec<usize>,
10    start_offset: usize,
11}
12
13impl Layout {
14    pub fn new(shape: Shape, stride: Vec<usize>, start_offset: usize) -> Self {
15        Self {
16            shape,
17            stride,
18            start_offset,
19        }
20    }
21
22    pub fn contiguous_with_offset<S: Into<Shape>>(shape: S, start_offset: usize) -> Self {
23        let shape = shape.into();
24        let stride = shape.stride_contiguous();
25        Self {
26            shape,
27            stride,
28            start_offset,
29        }
30    }
31
32    pub fn contiguous<S: Into<Shape>>(shape: S) -> Self {
33        Self::contiguous_with_offset(shape, 0)
34    }
35
36    pub fn dims(&self) -> &[usize] {
37        self.shape.dims()
38    }
39
40    /// The dimension size for a specified dimension index.
41    pub fn dim<D: crate::shape::Dim>(&self, dim: D) -> Result<usize> {
42        let dim = dim.to_index(&self.shape, "dim")?;
43        Ok(self.dims()[dim])
44    }
45
46    pub fn shape(&self) -> &Shape {
47        &self.shape
48    }
49
50    pub fn stride(&self) -> &[usize] {
51        &self.stride
52    }
53
54    pub fn start_offset(&self) -> usize {
55        self.start_offset
56    }
57
58    /// Returns outer stride along `dim` if valid.
59    ///
60    /// Two conditions must hold:
61    ///  1. Inner dims `[dim..]` has standard contiguous strides.
62    ///  2. Outer dims `[..dim]` are contiguous among themselves, i.e.
63    ///     `stride[k] == dims[k+1] * stride[k+1]` for `k` in `0..dim-1`.
64    ///
65    /// When the tensor is fully contiguous this returns `Some(dims[dim..].product())`.
66    pub(crate) fn outer_stride_for_dim(&self, dim: usize) -> Option<usize> {
67        let dims = self.dims();
68        let strides = self.stride();
69
70        // 1. Inner `dims[dim..]` must have contiguous strides.
71        let mut expected = 1usize;
72        for i in (dim..dims.len()).rev() {
73            if strides[i] != expected {
74                return None;
75            }
76            expected *= dims[i];
77        }
78
79        if dim == 0 {
80            // No outer dims.
81            // `expected = dims[dim..].product()`
82            return Some(expected);
83        }
84
85        // 2. Outer `dims[0..dim]` must be internally contiguous.
86        let outer_stride = strides[dim - 1];
87        let mut expected_outer = outer_stride;
88        for k in (0..dim - 1).rev() {
89            expected_outer *= dims[k + 1];
90            if strides[k] != expected_outer {
91                return None;
92            }
93        }
94
95        Some(outer_stride)
96    }
97
98    /// Checks if more than one logical index lands on the same cell (or when we can't prove it does not)
99    pub fn has_internal_overlap(&self) -> bool {
100        !self.range().is_some_and(|f| f.injective)
101    }
102
103    /// Returns range of cells this layout can reach, and wether it reaches all of them.
104    /// Returns `None` on arithmetic overflow.
105    fn range(&self) -> Option<LayoutRange> {
106        // Filter out dims <= 1 as their strides are irrelevant.
107        let mut axes: Vec<(usize, usize)> = self
108            .dims()
109            .iter()
110            .zip(self.stride())
111            .filter(|(&d, _)| d > 1)
112            .map(|(&d, &s)| (d, s))
113            .collect();
114        axes.sort_unstable_by_key(|&(_, s)| s);
115
116        let mut span = 0usize;
117        let mut injective = true;
118        let mut dense = true;
119
120        for (d, s) in axes {
121            if s <= span {
122                // This dim can land on a cell another dim already reaches.
123                injective = false;
124                dense = false;
125            } else if s - span != 1 {
126                // Has a gap
127                dense = false;
128            }
129            span = span.checked_add((d - 1).checked_mul(s)?)?;
130        }
131
132        let lo = self.start_offset();
133        Some(LayoutRange {
134            lo,
135            hi: lo.checked_add(span)?,
136            injective,
137            dense,
138        })
139    }
140
141    /// Relation between this layout and another
142    pub fn relation(&self, other: &Self) -> LayoutRelation {
143        if self == other {
144            return LayoutRelation::Identical;
145        }
146
147        if self.shape().elem_count() == 0 || other.shape().elem_count() == 0 {
148            return LayoutRelation::Disjoint;
149        }
150
151        // Extract [`LayoutRange`] from layout.
152        let (a, b) = match (self.range(), other.range()) {
153            (Some(a), Some(b)) => (a, b),
154            _ => return LayoutRelation::Unknown, // address arithmetic overflowed
155        };
156
157        if a.separated_from(&b) {
158            return LayoutRelation::Disjoint;
159        }
160
161        if a.densely_contains(&b) || b.densely_contains(&a) {
162            return LayoutRelation::Overlapping;
163        }
164
165        // We end up here when layouts ranges overlap and neither is dense, such as disjoint
166        // column slices. Figuring out these cases is a bounded integer feasibility problem
167        // over the strides. This is NP-hard in general. Cheap at common tensor ranks.
168        // If we want to move more cases out of unknown into disjoint/overlapping it can be done using the
169        // same approach as numpy: https://github.com/numpy/numpy/blob/main/numpy/_core/src/common/mem_overlap.c
170        LayoutRelation::Unknown
171    }
172
173    /// Returns the appropriate start and stop offset if the data is stored in a C
174    /// contiguous (aka row major) way.
175    pub fn contiguous_offsets(&self) -> Option<(usize, usize)> {
176        if self.is_contiguous() {
177            let start_o = self.start_offset;
178            Some((start_o, start_o + self.shape.elem_count()))
179        } else {
180            None
181        }
182    }
183
184    /// Returns true if the data is stored in a C contiguous (aka row major) way.
185    /// Note that this does not implies that the start offset is 0 or that there are no extra
186    /// elements at the end of the storage.
187    pub fn is_contiguous(&self) -> bool {
188        self.shape.is_contiguous(&self.stride)
189    }
190
191    /// Returns true if the data is stored in a Fortran contiguous (aka column major) way.
192    pub fn is_fortran_contiguous(&self) -> bool {
193        self.shape.is_fortran_contiguous(&self.stride)
194    }
195
196    pub fn is_scalar(&self) -> bool {
197        let dims = self.dims();
198        dims.is_empty() || dims.iter().all(|d| *d == 1)
199    }
200
201    /// Returns true if the data is actually a scalar during broadcast
202    pub fn is_scalar_broadcast(&self) -> bool {
203        self.stride().iter().all(|s| *s == 0)
204    }
205
206    pub fn is_scalar_like(&self) -> bool {
207        self.is_scalar() || self.is_scalar_broadcast()
208    }
209
210    pub fn narrow(&self, dim: usize, start: usize, len: usize) -> Result<Self> {
211        let dims = self.shape().dims();
212        if dim >= dims.len() {
213            Err(Error::DimOutOfRange {
214                shape: self.shape().clone(),
215                dim: dim as i32,
216                op: "narrow",
217            }
218            .bt())?
219        }
220        if start + len > dims[dim] {
221            Err(Error::NarrowInvalidArgs {
222                shape: self.shape.clone(),
223                dim,
224                start,
225                len,
226                msg: "start + len > dim_len",
227            }
228            .bt())?
229        }
230        let mut dims = dims.to_vec();
231        dims[dim] = len;
232        Ok(Self {
233            shape: Shape::from(dims),
234            stride: self.stride.clone(),
235            start_offset: self.start_offset + self.stride[dim] * start,
236        })
237    }
238
239    pub fn transpose(&self, dim1: usize, dim2: usize) -> Result<Self> {
240        let rank = self.shape.rank();
241        if rank <= dim1 || rank <= dim2 {
242            Err(Error::UnexpectedNumberOfDims {
243                expected: usize::max(dim1, dim2),
244                got: rank,
245                shape: self.shape().clone(),
246            }
247            .bt())?
248        }
249        let mut stride = self.stride().to_vec();
250        let mut dims = self.shape().dims().to_vec();
251        dims.swap(dim1, dim2);
252        stride.swap(dim1, dim2);
253        Ok(Self {
254            shape: Shape::from(dims),
255            stride,
256            start_offset: self.start_offset,
257        })
258    }
259
260    pub fn permute(&self, idxs: &[usize]) -> Result<Self> {
261        let is_permutation =
262            idxs.len() == self.shape.rank() && (0..idxs.len()).all(|i| idxs.contains(&i));
263        if !is_permutation {
264            crate::bail!(
265                "dimension mismatch in permute, tensor {:?}, dims: {:?}",
266                self.dims(),
267                idxs
268            )
269        }
270        let stride = self.stride();
271        let dims = self.shape().dims();
272        let mut perm_stride = stride.to_vec();
273        let mut perm_dims = dims.to_vec();
274        for (i, &idx) in idxs.iter().enumerate() {
275            perm_stride[i] = stride[idx];
276            perm_dims[i] = dims[idx];
277        }
278        Ok(Self {
279            shape: Shape::from(perm_dims),
280            stride: perm_stride,
281            start_offset: self.start_offset,
282        })
283    }
284
285    pub fn broadcast_as<S: Into<Shape>>(&self, shape: S) -> Result<Self> {
286        let shape = shape.into();
287        if shape.rank() < self.shape().rank() {
288            return Err(Error::BroadcastIncompatibleShapes {
289                src_shape: self.shape().clone(),
290                dst_shape: shape,
291            }
292            .bt());
293        }
294        let added_dims = shape.rank() - self.shape().rank();
295        let mut stride = vec![0; added_dims];
296        for (&dst_dim, (&src_dim, &src_stride)) in shape.dims()[added_dims..]
297            .iter()
298            .zip(self.dims().iter().zip(self.stride()))
299        {
300            let s = if dst_dim == src_dim {
301                src_stride
302            } else if src_dim != 1 {
303                return Err(Error::BroadcastIncompatibleShapes {
304                    src_shape: self.shape().clone(),
305                    dst_shape: shape,
306                }
307                .bt());
308            } else {
309                0
310            };
311            stride.push(s)
312        }
313        Ok(Self {
314            shape,
315            stride,
316            start_offset: self.start_offset,
317        })
318    }
319
320    pub(crate) fn strided_index(&self) -> crate::StridedIndex<'_> {
321        crate::StridedIndex::from_layout(self)
322    }
323
324    pub(crate) fn strided_blocks(&self) -> crate::StridedBlocks<'_> {
325        let mut block_len = 1usize;
326        let mut contiguous_dims = 0usize; // Counted from the right.
327        for (&stride, &dim) in self.stride().iter().zip(self.dims().iter()).rev() {
328            // Size-1 dimensions are trivially contiguous regardless of their stride.
329            if dim == 1 {
330                contiguous_dims += 1;
331                continue;
332            }
333            if stride != block_len {
334                break;
335            }
336            block_len *= dim;
337            contiguous_dims += 1;
338        }
339        let index_dims = self.dims().len() - contiguous_dims;
340        match index_dims {
341            0 => crate::StridedBlocks::SingleBlock {
342                start_offset: self.start_offset,
343                len: block_len,
344            },
345            1 => crate::StridedBlocks::UniformBlocks {
346                start_offset: self.start_offset,
347                block_len,
348                count: self.dims()[0],
349                src_stride: self.stride[0],
350            },
351            _ => {
352                let block_start_index = crate::StridedIndex::new(
353                    &self.dims()[..index_dims],
354                    &self.stride[..index_dims],
355                    self.start_offset,
356                );
357                crate::StridedBlocks::MultipleBlocks {
358                    block_start_index,
359                    block_len,
360                }
361            }
362        }
363    }
364}
365
366/// Describes the range of cells a [`Layout`] can reach within its allocation,
367/// and whether it reaches all of them.
368struct LayoutRange {
369    /// Lowest reachable cell. Equal to layout `start_offset`.
370    lo: usize,
371    /// Highest reachable cell (inclusive).
372    hi: usize,
373    /// Proven to map distinct logical indices to distinct cells.
374    injective: bool,
375    /// Indicates that layout occupies every cell in `lo..=hi`.
376    /// Does not necessarily mean that layout is contiguous.
377    dense: bool,
378}
379
380impl LayoutRange {
381    /// Bounding intervals cannot meet, which means these layouts are seperate.
382    fn separated_from(&self, other: &Self) -> bool {
383        self.hi < other.lo || other.hi < self.lo
384    }
385
386    /// If a dense layout contains another's `lo` there is overlap.
387    fn densely_contains(&self, other: &Self) -> bool {
388        self.dense && other.lo >= self.lo && other.lo <= self.hi
389    }
390}
391
392/// How two layouts over the same allocation relate.
393#[derive(Copy, Clone, PartialEq, Eq, Debug)]
394pub enum LayoutRelation {
395    /// Simple assignment. x[i] += x[i]
396    Identical,
397    /// Completely distinct layouts.
398    Disjoint,
399    /// Any kind of overlap.
400    Overlapping,
401    /// Could not prove relation.
402    Unknown,
403}