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}