Skip to main content

rlx_cpu/
tile.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//! CPU `TileIO` impls (plans #23 + #27).
6//!
7//! Borrowed from MAX's
8//! `structured_kernels/{kernel_common, tile_types, smem_types}.mojo`
9//! and `layout/tile_io.mojo`. Lifts the "kernel-author standard
10//! library" pattern: typed primitives kernels compose, instead of
11//! re-deriving stride math and load/store loops per kernel.
12//!
13//! The vocabulary types (`Tile2`, `Coord2`, `Strides2`) live in
14//! `rlx_ir::layout` (plan #3 — shared layout IR) so Metal kernels
15//! can use the same names. CPU-specific `TileIO` impls live here.
16
17pub use rlx_ir::{Coord2, Strides2, Tile2};
18
19/// Tile I/O trait — load / store / prefetch parameterized over the
20/// physical layout. Two impls today: [`RowMajorTile`] (the standard
21/// flat layout) and [`StridedTile`] (when reading a non-contiguous
22/// view, e.g. last-axis Narrow into Attention).
23///
24/// Methods take pointers (not slices) so the abstraction works for
25/// both owned and aliased buffers.
26pub trait TileIO {
27    /// Compute the byte address for a coordinate. Used by
28    /// `load` / `store` / `prefetch` so impls only need to define
29    /// the address arithmetic once.
30    /// SAFETY: caller checks bounds.
31    unsafe fn address(&self, base: *const f32, c: Coord2) -> *const f32;
32
33    /// Load a tile element by `(row, col)`.
34    /// SAFETY: caller ensures the address is valid for read.
35    #[inline(always)]
36    unsafe fn load(&self, base: *const f32, c: Coord2) -> f32 {
37        unsafe { *self.address(base, c) }
38    }
39
40    /// Store an element by `(row, col)`.
41    /// SAFETY: caller ensures the address is valid for write.
42    #[inline(always)]
43    unsafe fn store(&self, base: *mut f32, c: Coord2, v: f32) {
44        unsafe {
45            *(self.address(base, c) as *mut f32) = v;
46        }
47    }
48
49    /// Hint to the prefetcher. On aarch64 issues a single
50    /// `prfm pldl1keep` (load into L1, retain). Elsewhere a no-op.
51    /// SAFETY: caller ensures the address is in a valid mapping.
52    #[inline(always)]
53    unsafe fn prefetch(&self, base: *const f32, c: Coord2) {
54        unsafe {
55            let addr = self.address(base, c);
56            #[cfg(target_arch = "aarch64")]
57            {
58                std::arch::asm!("prfm pldl1keep, [{0}]", in(reg) addr,
59                    options(nostack, readonly));
60            }
61            #[cfg(not(target_arch = "aarch64"))]
62            {
63                let _ = addr;
64            }
65        }
66    }
67}
68
69/// Row-major contiguous tile: `addr = base + row * cols + col`.
70#[derive(Debug, Clone, Copy)]
71pub struct RowMajorTile {
72    pub shape: Tile2,
73}
74
75impl TileIO for RowMajorTile {
76    #[inline(always)]
77    unsafe fn address(&self, base: *const f32, c: Coord2) -> *const f32 {
78        unsafe { base.add(c.row * self.shape.cols + c.col) }
79    }
80}
81
82/// Strided tile: each row stride is configurable. Lets a kernel
83/// read a non-contiguous view (e.g. last-axis Narrow output) with
84/// the same TileIO interface as a contiguous tile.
85#[derive(Debug, Clone, Copy)]
86pub struct StridedTile {
87    pub shape: Tile2,
88    pub strides: Strides2,
89}
90
91impl TileIO for StridedTile {
92    #[inline(always)]
93    unsafe fn address(&self, base: *const f32, c: Coord2) -> *const f32 {
94        unsafe { base.add(c.row * self.strides.row + c.col * self.strides.col) }
95    }
96}
97
98/// Walk every element of a tile in row-major order, calling `f`.
99/// Convenience for kernels that don't care about iteration order.
100#[inline(always)]
101pub fn for_each_coord(shape: Tile2, mut f: impl FnMut(Coord2)) {
102    for r in 0..shape.rows {
103        for c in 0..shape.cols {
104            f(Coord2 { row: r, col: c });
105        }
106    }
107}
108
109/// Tile copy via TileIO. Source and destination layouts can differ
110/// (the typical use: read strided source, write contiguous dst).
111///
112/// # Safety
113/// `src_base` and `dst_base` must point into allocations large enough
114/// for `shape`'s extents under the IO layouts in `src_io` / `dst_io`.
115/// The two ranges may not overlap.
116#[inline]
117pub unsafe fn copy_tile<S: TileIO, D: TileIO>(
118    src_io: &S,
119    src_base: *const f32,
120    dst_io: &D,
121    dst_base: *mut f32,
122    shape: Tile2,
123) {
124    for_each_coord(shape, |c| unsafe {
125        dst_io.store(dst_base, c, src_io.load(src_base, c));
126    });
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn row_major_round_trip() {
135        let mut buf = [0f32; 12]; // 3×4
136        let io = RowMajorTile {
137            shape: Tile2::new(3, 4),
138        };
139        unsafe {
140            io.store(buf.as_mut_ptr(), Coord2 { row: 1, col: 2 }, 42.0);
141            assert_eq!(io.load(buf.as_ptr(), Coord2 { row: 1, col: 2 }), 42.0);
142        }
143        assert_eq!(buf[4 + 2], 42.0);
144    }
145
146    #[test]
147    fn strided_reads_non_contig_view() {
148        // Source: 4-row tile inside a 4-row × 8-col parent.
149        // Pretending we narrowed cols 2..6 of each row; row stride = 8.
150        let parent: Vec<f32> = (0..32).map(|i| i as f32).collect();
151        let view = StridedTile {
152            shape: Tile2::new(4, 4),
153            strides: Strides2 { row: 8, col: 1 },
154        };
155        // base pointer offset to col=2 of row 0
156        let base = unsafe { parent.as_ptr().add(2) };
157        let v = unsafe { view.load(base, Coord2 { row: 1, col: 1 }) };
158        // expected: parent[1*8 + 2 + 1] = 11
159        assert_eq!(v, 11.0);
160    }
161
162    #[test]
163    fn prefetch_doesnt_panic() {
164        // Prefetch is a hint — it should not crash, and should
165        // accept any in-bounds address. We just verify the call
166        // sequence compiles + runs on the current target.
167        let buf = vec![0f32; 64];
168        let io = RowMajorTile {
169            shape: Tile2::new(8, 8),
170        };
171        unsafe {
172            io.prefetch(buf.as_ptr(), Coord2 { row: 0, col: 0 });
173            io.prefetch(buf.as_ptr(), Coord2 { row: 7, col: 7 });
174        }
175    }
176
177    #[test]
178    fn copy_tile_strided_to_contig() {
179        let parent: Vec<f32> = (0..32).map(|i| i as f32).collect();
180        let mut dst = vec![0f32; 16]; // 4×4 contiguous
181        let src_io = StridedTile {
182            shape: Tile2::new(4, 4),
183            strides: Strides2 { row: 8, col: 1 },
184        };
185        let dst_io = RowMajorTile {
186            shape: Tile2::new(4, 4),
187        };
188        let base = unsafe { parent.as_ptr().add(2) };
189        unsafe {
190            copy_tile(&src_io, base, &dst_io, dst.as_mut_ptr(), Tile2::new(4, 4));
191        }
192        // First row of dst should be parent[2..6] = [2,3,4,5].
193        assert_eq!(&dst[0..4], &[2.0, 3.0, 4.0, 5.0]);
194        assert_eq!(&dst[4..8], &[10.0, 11.0, 12.0, 13.0]);
195    }
196}