Skip to main content

rlx_ir/
layout.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Shared layout vocabulary (plan #3).
6//!
7//! Tile / coordinate / stride types used by every kernel-author
8//! crate. Lives in `rlx-ir` (the leaf) so CPU and Metal stop
9//! re-deriving stride math independently.
10//!
11//! Backend-specific I/O (CPU pointer reads, Metal threadgroup
12//! loads) lives in the backend's own crate behind a `TileIO` trait
13//! — only the *vocabulary* is shared here.
14
15/// 2-D row-major or strided tile shape (in elements).
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct Tile2 {
18    pub rows: usize,
19    pub cols: usize,
20}
21
22impl Tile2 {
23    pub const fn new(rows: usize, cols: usize) -> Self {
24        Self { rows, cols }
25    }
26    pub const fn area(self) -> usize {
27        self.rows * self.cols
28    }
29}
30
31/// 2-D coordinate within a tile.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct Coord2 {
34    pub row: usize,
35    pub col: usize,
36}
37
38/// Per-axis strides in **elements** (not bytes). `row` is the
39/// distance between consecutive rows; `col` between consecutive
40/// columns. For a contiguous row-major tile of shape (R, C):
41/// `Strides2 { row: C, col: 1 }`.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct Strides2 {
44    pub row: usize,
45    pub col: usize,
46}
47
48impl Strides2 {
49    pub const fn row_major(cols: usize) -> Self {
50        Self { row: cols, col: 1 }
51    }
52    pub const fn col_major(rows: usize) -> Self {
53        Self { row: 1, col: rows }
54    }
55}
56
57/// Hierarchical shape tuple (plan #38). Borrowed from MAX's
58/// `layout/int_tuple.mojo`: shapes nest, so a `((B, S), (H, D))`
59/// expression captures the "outer batch+seq, inner heads+head_dim"
60/// structure of a tiled layout. Useful for kernels that want to
61/// reason about block-tiled sweeps without re-deriving the
62/// implied stride math each time.
63///
64/// Stays alongside the existing flat [`crate::Shape`] (which is
65/// what every op carries today). New code that benefits from
66/// hierarchy uses [`ShapeTuple`]; we don't migrate Shape because
67/// the entire codebase is built around it and the win is
68/// concentrated in advanced layout / fusion code.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum ShapeTuple {
71    /// Single concrete dimension.
72    Leaf(usize),
73    /// Ordered list of sub-tuples. Nesting is unbounded.
74    Nested(Vec<ShapeTuple>),
75}
76
77impl ShapeTuple {
78    /// One-dim leaf. `ShapeTuple::leaf(8)`.
79    pub fn leaf(n: usize) -> Self {
80        Self::Leaf(n)
81    }
82
83    /// Wrapping constructor for nested layouts.
84    pub fn nested(parts: Vec<ShapeTuple>) -> Self {
85        Self::Nested(parts)
86    }
87
88    /// Convenience: build a flat tuple from `&[usize]`. Each
89    /// element becomes a `Leaf`. `flat(&[2, 3, 4])` is equivalent
90    /// to `Nested(vec![Leaf(2), Leaf(3), Leaf(4)])`.
91    pub fn flat(dims: &[usize]) -> Self {
92        Self::Nested(dims.iter().map(|&n| Self::Leaf(n)).collect())
93    }
94
95    pub fn is_leaf(&self) -> bool {
96        matches!(self, Self::Leaf(_))
97    }
98
99    /// Top-level rank. Leaves are rank 1; nested tuples are the
100    /// length of the outer list.
101    pub fn rank(&self) -> usize {
102        match self {
103            Self::Leaf(_) => 1,
104            Self::Nested(v) => v.len(),
105        }
106    }
107
108    /// Total element count, traversing the entire hierarchy.
109    pub fn product(&self) -> usize {
110        match self {
111            Self::Leaf(n) => *n,
112            Self::Nested(v) => v.iter().map(|p| p.product()).product(),
113        }
114    }
115
116    /// Flatten into a row-major sequence of leaves. Useful when
117    /// converting to the existing `Shape` type.
118    pub fn flatten(&self) -> Vec<usize> {
119        let mut out = Vec::new();
120        self.flatten_into(&mut out);
121        out
122    }
123
124    fn flatten_into(&self, out: &mut Vec<usize>) {
125        match self {
126            Self::Leaf(n) => out.push(*n),
127            Self::Nested(v) => v.iter().for_each(|p| p.flatten_into(out)),
128        }
129    }
130
131    /// Walk a path of indices through the hierarchy. Returns
132    /// the sub-tuple at `path` or `None` if the path goes out of
133    /// bounds at any level.
134    ///
135    /// `[]` returns `Some(self)`; `[0]` returns the first child.
136    pub fn get(&self, path: &[usize]) -> Option<&ShapeTuple> {
137        if path.is_empty() {
138            return Some(self);
139        }
140        match self {
141            Self::Leaf(_) => None, // can't descend into a leaf
142            Self::Nested(v) => v.get(path[0]).and_then(|c| c.get(&path[1..])),
143        }
144    }
145}
146
147/// Ragged-tensor descriptor (plan #4). Represents a tensor of
148/// variable-length sequences laid out without padding:
149///
150///   data:    [total_elems, trailing_dim]   flat
151///   offsets: [batch + 1]                    cumulative starts
152///
153/// `data[offsets[i]..offsets[i+1]]` is row `i`'s contents (each
154/// row has `(offsets[i+1] - offsets[i])` elements times trailing).
155///
156/// Borrowed from MAX's `nn/_ragged_utils.mojo`, `kv_cache_ragged.mojo`,
157/// and `gemv_partial_norm.mojo`. Essential for serving throughput when
158/// sequences in a batch have very different lengths — padding to max
159/// wastes most of the work; ragged + offset-driven kernels process each
160/// row at its actual length.
161///
162/// Today this is the type vocabulary; kernel paths come per-op as
163/// the ragged use-case lands (the cumsum primitive #44 already
164/// covers offset construction).
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub struct Ragged {
167    /// Number of rows (= batch).
168    pub rows: usize,
169    /// Trailing per-element width. For BERT it's the hidden
170    /// dimension; for KV cache it's `num_heads * head_dim`. 1 if
171    /// the tensor is a flat sequence of scalars.
172    pub trailing: usize,
173    /// Total elements across all rows (sum of per-row lengths).
174    /// Equals `offsets[rows]` when offsets are materialized.
175    pub total: usize,
176}
177
178impl Ragged {
179    pub const fn new(rows: usize, trailing: usize, total: usize) -> Self {
180        Self {
181            rows,
182            trailing,
183            total,
184        }
185    }
186
187    /// Total f32 element count (data) — does not count the offsets
188    /// table.
189    pub const fn data_elements(self) -> usize {
190        self.total * self.trailing
191    }
192
193    /// Element count of the offsets table (`rows + 1`).
194    pub const fn offsets_elements(self) -> usize {
195        self.rows + 1
196    }
197}
198
199/// 3-D extension for `[batch, rows, cols]` tiles. Common for
200/// per-head attention sweeps.
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub struct Tile3 {
203    pub batch: usize,
204    pub rows: usize,
205    pub cols: usize,
206}
207
208impl Tile3 {
209    pub const fn new(batch: usize, rows: usize, cols: usize) -> Self {
210        Self { batch, rows, cols }
211    }
212    pub const fn area(self) -> usize {
213        self.batch * self.rows * self.cols
214    }
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub struct Strides3 {
219    pub batch: usize,
220    pub row: usize,
221    pub col: usize,
222}
223
224impl Strides3 {
225    pub const fn row_major(rows: usize, cols: usize) -> Self {
226        Self {
227            batch: rows * cols,
228            row: cols,
229            col: 1,
230        }
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn tile2_area() {
240        assert_eq!(Tile2::new(3, 4).area(), 12);
241    }
242
243    #[test]
244    fn strides2_presets() {
245        assert_eq!(Strides2::row_major(8), Strides2 { row: 8, col: 1 });
246        assert_eq!(Strides2::col_major(8), Strides2 { row: 1, col: 8 });
247    }
248
249    #[test]
250    fn strides3_row_major() {
251        assert_eq!(
252            Strides3::row_major(3, 4),
253            Strides3 {
254                batch: 12,
255                row: 4,
256                col: 1
257            }
258        );
259    }
260
261    // Tuple tests live here so `tuple` test names cover the new
262    // hierarchical type (the runtime check covers the const fns).
263    #[test]
264    fn tuple_leaf_constructors() {
265        let a = ShapeTuple::leaf(8);
266        assert_eq!(a.flatten(), vec![8]);
267        assert_eq!(a.product(), 8);
268        assert!(a.is_leaf());
269    }
270
271    #[test]
272    fn tuple_flat_constructor() {
273        let s = ShapeTuple::flat(&[2, 3, 4]);
274        assert_eq!(s.flatten(), vec![2, 3, 4]);
275        assert_eq!(s.product(), 24);
276        assert_eq!(s.rank(), 3);
277    }
278
279    #[test]
280    fn tuple_nested_product_and_flatten() {
281        // BERT-shape: ((batch, seq), (heads, head_dim)).
282        let bs = ShapeTuple::nested(vec![ShapeTuple::leaf(8), ShapeTuple::leaf(15)]);
283        let nh = ShapeTuple::nested(vec![ShapeTuple::leaf(12), ShapeTuple::leaf(64)]);
284        let s = ShapeTuple::nested(vec![bs, nh]);
285        assert_eq!(s.product(), 8 * 15 * 12 * 64);
286        assert_eq!(s.flatten(), vec![8, 15, 12, 64]);
287        assert_eq!(s.rank(), 2); // top-level rank
288    }
289
290    #[test]
291    fn tuple_get_resolves_path() {
292        let inner = ShapeTuple::nested(vec![ShapeTuple::leaf(12), ShapeTuple::leaf(64)]);
293        let s = ShapeTuple::nested(vec![ShapeTuple::leaf(8), ShapeTuple::leaf(15), inner]);
294        assert_eq!(s.get(&[0]), Some(&ShapeTuple::Leaf(8)));
295        assert_eq!(s.get(&[2, 1]), Some(&ShapeTuple::Leaf(64)));
296        assert_eq!(s.get(&[2, 99]), None);
297    }
298
299    #[test]
300    fn ragged_element_counts() {
301        // 4 rows with total 30 elements; trailing = 8 (hidden dim).
302        let r = Ragged::new(4, 8, 30);
303        assert_eq!(r.data_elements(), 240); // 30 * 8 floats
304        assert_eq!(r.offsets_elements(), 5); // rows + 1
305    }
306}