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}