Skip to main content

hurray_core/layout/
tiled.rs

1//! Tiled / blocked layout descriptor.
2//!
3//! Tag `0x04`. Carries tile shape, outer/inner layout tags, and optional
4//! outer/inner explicit strides. The inner layout MAY itself be tiled,
5//! making this structure recursive (max depth 8 per spec).
6//! See `docs/spec/layouts/tiled.md`.
7
8use crate::{Error, Result};
9
10/// Maximum recursion depth for nested tiled layouts.
11///
12/// The spec RECOMMENDS a maximum of 8 levels; this implementation enforces it.
13pub const MAX_TILED_DEPTH: usize = 8;
14
15/// Outer strides for the tile grid (in units of **tiles**, not elements).
16///
17/// Present only when `outer_layout == 0x03` (strided).
18///
19/// # Examples
20///
21/// ```
22/// use hurray_core::layout::OuterStrides;
23///
24/// let os = OuterStrides::new(vec![2, 1]);
25/// assert_eq!(os.strides, [2, 1]);
26/// ```
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28#[non_exhaustive]
29pub struct OuterStrides {
30    /// Outer tile-grid strides, one per dimension, in units of **tiles**.
31    pub strides: Vec<i64>,
32}
33
34impl OuterStrides {
35    /// Creates a new [`OuterStrides`] with the given per-dimension tile-grid strides.
36    ///
37    /// # Examples
38    ///
39    /// ```
40    /// use hurray_core::layout::OuterStrides;
41    ///
42    /// let os = OuterStrides::new(vec![3, 1]);
43    /// assert_eq!(os.strides, [3, 1]);
44    /// ```
45    pub fn new(strides: Vec<i64>) -> Self {
46        Self { strides }
47    }
48}
49
50/// Inner strides within a single tile (in **logical elements**).
51///
52/// Present only when `inner_layout == 0x03` (strided).
53///
54/// # Examples
55///
56/// ```
57/// use hurray_core::layout::InnerStrides;
58///
59/// let is = InnerStrides::new(vec![4, 1]);
60/// assert_eq!(is.strides, [4, 1]);
61/// ```
62#[derive(Debug, Clone, PartialEq, Eq, Hash)]
63#[non_exhaustive]
64pub struct InnerStrides {
65    /// Per-dimension strides within a tile, in **logical elements**.
66    pub strides: Vec<i64>,
67}
68
69impl InnerStrides {
70    /// Creates a new [`InnerStrides`] with the given per-dimension element strides.
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use hurray_core::layout::InnerStrides;
76    ///
77    /// let is = InnerStrides::new(vec![1, 4]);
78    /// assert_eq!(is.strides, [1, 4]);
79    /// ```
80    pub fn new(strides: Vec<i64>) -> Self {
81        Self { strides }
82    }
83}
84
85/// Descriptor for the tiled / blocked layout.
86///
87/// A tiled layout partitions the tensor index space into uniform rectangular
88/// tiles. Tile ordering is controlled by `outer_layout`; element ordering
89/// within a tile is controlled by `inner_layout`. Both accept tags for
90/// row-major (`0x01`), column-major (`0x02`), strided (`0x03`), or, for the
91/// inner layout only, a recursively nested tiled layout (`0x04`).
92///
93/// Recursive tiling is useful for hierarchical GEMM blocking (e.g., 128×128
94/// L2 tiles subdivided into 32×32 L1 tiles). Maximum nesting depth is
95/// [`MAX_TILED_DEPTH`] (8 levels).
96///
97/// # Examples
98///
99/// ```
100/// use hurray_core::layout::{LayoutDescriptor, TiledLayout};
101///
102/// // 2×4 tiles, row-major outer, row-major inner.
103/// let layout = LayoutDescriptor::Tiled(Box::new(
104///     TiledLayout::new(vec![2, 4], 0x01, 0x01, None, None, None).unwrap(),
105/// ));
106/// assert_eq!(layout.tag(), 0x04);
107/// ```
108#[derive(Debug, Clone, PartialEq, Eq, Hash)]
109#[non_exhaustive]
110pub struct TiledLayout {
111    /// Tile dimensions. Every value MUST be > 0.
112    /// `tile_shape.len()` MUST equal the tensor rank.
113    pub tile_shape: Vec<u64>,
114
115    /// Layout tag for tile-grid ordering.
116    /// MUST be `0x01` (row-major), `0x02` (col-major), or `0x03` (strided).
117    pub outer_layout: u8,
118
119    /// Layout tag for element ordering within a tile.
120    /// MUST be `0x01`, `0x02`, `0x03`, or `0x04` (recursive tiling).
121    pub inner_layout: u8,
122
123    /// Explicit outer (tile-grid) strides in units of **tiles**.
124    /// Present iff `outer_layout == 0x03`.
125    pub outer_strides: Option<OuterStrides>,
126
127    /// Explicit inner strides in **logical elements** within a tile.
128    /// Present iff `inner_layout == 0x03`.
129    pub inner_strides: Option<InnerStrides>,
130
131    /// Recursive inner tiling descriptor.
132    /// Present iff `inner_layout == 0x04`.
133    pub inner_tiled: Option<Box<TiledLayout>>,
134}
135
136impl TiledLayout {
137    /// Creates a new [`TiledLayout`], validating that:
138    /// - all `tile_shape` values are > 0,
139    /// - `outer_layout` is `0x01`, `0x02`, or `0x03`,
140    /// - `inner_layout` is `0x01`, `0x02`, `0x03`, or `0x04`,
141    /// - `outer_strides` is `Some` iff `outer_layout == 0x03`,
142    /// - `inner_strides` is `Some` iff `inner_layout == 0x03`,
143    /// - `inner_tiled` is `Some` iff `inner_layout == 0x04`,
144    /// - recursion depth does not exceed [`MAX_TILED_DEPTH`].
145    ///
146    /// # Errors
147    ///
148    /// Returns [`Error::InvalidLayout`] on any constraint violation.
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// use hurray_core::layout::TiledLayout;
154    ///
155    /// // 4×4 tiles, row-major outer, column-major inner.
156    /// let t = TiledLayout::new(vec![4, 4], 0x01, 0x02, None, None, None).unwrap();
157    /// assert_eq!(t.tile_shape, [4, 4]);
158    /// ```
159    pub fn new(
160        tile_shape: Vec<u64>,
161        outer_layout: u8,
162        inner_layout: u8,
163        outer_strides: Option<OuterStrides>,
164        inner_strides: Option<InnerStrides>,
165        inner_tiled: Option<Box<TiledLayout>>,
166    ) -> Result<Self> {
167        Self::new_at_depth(
168            tile_shape,
169            outer_layout,
170            inner_layout,
171            outer_strides,
172            inner_strides,
173            inner_tiled,
174            0,
175        )
176    }
177
178    // Internal constructor that tracks recursion depth.
179    fn new_at_depth(
180        tile_shape: Vec<u64>,
181        outer_layout: u8,
182        inner_layout: u8,
183        outer_strides: Option<OuterStrides>,
184        inner_strides: Option<InnerStrides>,
185        inner_tiled: Option<Box<TiledLayout>>,
186        depth: usize,
187    ) -> Result<Self> {
188        if depth >= MAX_TILED_DEPTH {
189            return Err(Error::InvalidLayout(format!(
190                "tiled layout recursion depth {depth} exceeds maximum of {MAX_TILED_DEPTH}"
191            )));
192        }
193
194        // All tile dimensions must be > 0.
195        if tile_shape.is_empty() {
196            return Err(Error::InvalidLayout(
197                "tile_shape must not be empty".to_string(),
198            ));
199        }
200        for (k, &dim) in tile_shape.iter().enumerate() {
201            if dim == 0 {
202                return Err(Error::InvalidLayout(format!(
203                    "tile_shape[{k}] must be > 0, got 0"
204                )));
205            }
206        }
207
208        // outer_layout: must be 0x01, 0x02, or 0x03.
209        if !matches!(outer_layout, 0x01..=0x03) {
210            return Err(Error::InvalidLayout(format!(
211                "outer_layout 0x{outer_layout:02X} is invalid: must be 0x01, 0x02, or 0x03"
212            )));
213        }
214
215        // inner_layout: must be 0x01, 0x02, 0x03, or 0x04.
216        if !matches!(inner_layout, 0x01..=0x04) {
217            return Err(Error::InvalidLayout(format!(
218                "inner_layout 0x{inner_layout:02X} is invalid: must be 0x01, 0x02, 0x03, or 0x04"
219            )));
220        }
221
222        // outer_strides must be present iff outer_layout == 0x03.
223        match (outer_layout, outer_strides.is_some()) {
224            (0x03, false) => {
225                return Err(Error::InvalidLayout(
226                    "outer_strides must be present when outer_layout is 0x03 (strided)".to_string(),
227                ));
228            }
229            (layout, true) if layout != 0x03 => {
230                return Err(Error::InvalidLayout(format!(
231                    "outer_strides present but outer_layout is 0x{layout:02X} (not strided)"
232                )));
233            }
234            _ => {}
235        }
236
237        // inner_strides must be present iff inner_layout == 0x03.
238        match (inner_layout, inner_strides.is_some()) {
239            (0x03, false) => {
240                return Err(Error::InvalidLayout(
241                    "inner_strides must be present when inner_layout is 0x03 (strided)".to_string(),
242                ));
243            }
244            (layout, true) if layout != 0x03 => {
245                return Err(Error::InvalidLayout(format!(
246                    "inner_strides present but inner_layout is 0x{layout:02X} (not strided)"
247                )));
248            }
249            _ => {}
250        }
251
252        // inner_tiled must be present iff inner_layout == 0x04.
253        match (inner_layout, inner_tiled.is_some()) {
254            (0x04, false) => {
255                return Err(Error::InvalidLayout(
256                    "inner_tiled must be present when inner_layout is 0x04 (tiled)".to_string(),
257                ));
258            }
259            (layout, true) if layout != 0x04 => {
260                return Err(Error::InvalidLayout(format!(
261                    "inner_tiled present but inner_layout is 0x{layout:02X} (not tiled)"
262                )));
263            }
264            _ => {}
265        }
266
267        // Recursively validate the inner tiled descriptor at depth+1.
268        // We rebuild it via new_at_depth to enforce depth tracking.
269        let inner_tiled = if let Some(inner) = inner_tiled {
270            // The inner TiledLayout was already validated by its own constructor;
271            // we only need to check that adding it here doesn't exceed max depth.
272            // Re-validate with depth+1 to enforce the global depth limit.
273            let validated = Self::new_at_depth(
274                inner.tile_shape.clone(),
275                inner.outer_layout,
276                inner.inner_layout,
277                inner.outer_strides.clone(),
278                inner.inner_strides.clone(),
279                inner.inner_tiled.clone(),
280                depth + 1,
281            )?;
282            Some(Box::new(validated))
283        } else {
284            None
285        };
286
287        Ok(Self {
288            tile_shape,
289            outer_layout,
290            inner_layout,
291            outer_strides,
292            inner_strides,
293            inner_tiled,
294        })
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301    use crate::layout::LayoutDescriptor;
302
303    #[test]
304    fn tiled_tag_is_0x04() {
305        let t = TiledLayout::new(vec![4, 4], 0x01, 0x01, None, None, None).unwrap();
306        let layout = LayoutDescriptor::Tiled(Box::new(t));
307        assert_eq!(layout.tag(), 0x04);
308    }
309
310    #[test]
311    fn tiled_buffer_count_is_1() {
312        use std::num::NonZeroU8;
313        let t = TiledLayout::new(vec![4, 4], 0x01, 0x01, None, None, None).unwrap();
314        let layout = LayoutDescriptor::Tiled(Box::new(t));
315        assert_eq!(layout.buffer_count(), Some(NonZeroU8::new(1).unwrap()));
316    }
317
318    #[test]
319    fn rejects_zero_tile_dim() {
320        let err = TiledLayout::new(vec![4, 0], 0x01, 0x01, None, None, None).unwrap_err();
321        assert!(matches!(err, Error::InvalidLayout(_)));
322    }
323
324    #[test]
325    fn rejects_invalid_outer_layout() {
326        let err = TiledLayout::new(vec![4, 4], 0x04, 0x01, None, None, None).unwrap_err();
327        assert!(matches!(err, Error::InvalidLayout(_)));
328    }
329
330    #[test]
331    fn rejects_invalid_inner_layout() {
332        let err = TiledLayout::new(vec![4, 4], 0x01, 0x05, None, None, None).unwrap_err();
333        assert!(matches!(err, Error::InvalidLayout(_)));
334    }
335
336    #[test]
337    fn rejects_missing_outer_strides_when_outer_is_strided() {
338        let err = TiledLayout::new(vec![4, 4], 0x03, 0x01, None, None, None).unwrap_err();
339        assert!(matches!(err, Error::InvalidLayout(_)));
340    }
341
342    #[test]
343    fn rejects_outer_strides_when_outer_not_strided() {
344        let outer_strides = Some(OuterStrides {
345            strides: vec![2, 1],
346        });
347        let err = TiledLayout::new(vec![4, 4], 0x01, 0x01, outer_strides, None, None).unwrap_err();
348        assert!(matches!(err, Error::InvalidLayout(_)));
349    }
350
351    #[test]
352    fn accepts_strided_outer_with_outer_strides() {
353        let outer_strides = Some(OuterStrides {
354            strides: vec![2, 1],
355        });
356        let t = TiledLayout::new(vec![4, 4], 0x03, 0x01, outer_strides, None, None);
357        assert!(t.is_ok());
358    }
359
360    #[test]
361    fn rejects_excessive_recursion_depth() {
362        // Build a chain of TiledLayouts from the leaf up.
363        // The leaf uses inner_layout=0x01 (row-major, no recursion).
364        // Each wrapper uses inner_layout=0x04 (tiled, recurses into inner).
365        fn make_tiled_at_depth(remaining: usize) -> Box<TiledLayout> {
366            if remaining == 0 {
367                // Leaf: no recursion.
368                Box::new(TiledLayout::new(vec![2, 2], 0x01, 0x01, None, None, None).unwrap())
369            } else {
370                let inner = make_tiled_at_depth(remaining - 1);
371                Box::new(TiledLayout::new(vec![2, 2], 0x01, 0x04, None, None, Some(inner)).unwrap())
372            }
373        }
374
375        // MAX_TILED_DEPTH - 1 wrappers + 1 leaf = MAX_TILED_DEPTH total levels → valid.
376        let _valid = make_tiled_at_depth(MAX_TILED_DEPTH - 1);
377
378        // One more wrapper pushes total depth to MAX_TILED_DEPTH + 1 → rejected.
379        let too_deep = TiledLayout::new(
380            vec![2, 2],
381            0x01,
382            0x04,
383            None,
384            None,
385            Some(make_tiled_at_depth(MAX_TILED_DEPTH - 1)),
386        );
387        assert!(
388            too_deep.is_err(),
389            "depth {} should be rejected (max is {})",
390            MAX_TILED_DEPTH + 1,
391            MAX_TILED_DEPTH
392        );
393    }
394}