rlx_driver/arena.rs
1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, version 3.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! Backend-agnostic arena trait — the contract every backend's memory
17//! plan obeys.
18//!
19//! Lifted from CpuExecutable / MetalExecutable's previously duplicated
20//! arena helpers. Each new backend (CUDA, ROCm, wgpu, WASM, TPU) implements
21//! this trait once and gets:
22//! - typed input feed (`f32 → arena_dtype`)
23//! - typed output read (`arena_dtype → f32`)
24//! - per-node byte offset resolution
25//!
26//! The trait deliberately exposes raw pointers / byte offsets rather than
27//! Rust slices so the same implementation works for host-resident memory
28//! (CPU/WASM), unified memory (Apple Silicon Metal/MPSGraph), and
29//! discrete-VRAM backends (CUDA/ROCm) where reading involves a copy.
30
31use rlx_ir::{DType, NodeId};
32
33/// Per-backend arena interface.
34///
35/// All concrete arenas — `rlx-cpu::Arena`, `rlx-metal::Arena`, future
36/// `rlx-cuda::Arena`, `rlx-wgpu::Arena` — implement this trait so the
37/// runtime can drive them uniformly. The actual byte layout is owned
38/// by the backend; we only require offset-based access.
39pub trait DeviceArena {
40 /// Byte offset of `id`'s buffer slot in the arena. `usize::MAX` for
41 /// nodes that don't have an arena slot (e.g. fused-away intermediates).
42 fn byte_offset(&self, id: NodeId) -> usize;
43
44 /// True if `id` has a real arena slot.
45 fn has_buffer(&self, id: NodeId) -> bool;
46
47 /// Total arena size in bytes.
48 fn size_bytes(&self) -> usize;
49
50 /// Write a host-side `f32` slice into `id`'s slot, casting to `dtype`
51 /// if necessary. Truncates to the buffer's capacity (no panic on overflow).
52 ///
53 /// For discrete-memory backends this involves a host→device copy; for
54 /// unified-memory backends (Apple Silicon, integrated GPUs) it's a
55 /// direct write.
56 fn write_input_f32(&mut self, id: NodeId, dtype: DType, data: &[f32]);
57
58 /// Read `id`'s slot as a host-side `Vec<f32>`, casting from `dtype` if
59 /// necessary. The number of elements is determined by the backend
60 /// based on the memory plan (typically `shape.num_elements()`).
61 fn read_output_f32(&self, id: NodeId, dtype: DType, n_elements: usize) -> Vec<f32>;
62}
63
64/// Helper: cast f32 input to bytes of `dtype` and write to `dst_ptr`.
65/// Used by every CPU-resident-arena backend. GPU backends can call this
66/// after staging into a host buffer, then upload.
67///
68/// Currently supports F32 / F64 / F16 / BF16 / C64. Other dtypes fall
69/// through to F32.
70pub unsafe fn write_typed_from_f32(dst_ptr: *mut u8, dtype: DType, src: &[f32], max_elems: usize) {
71 let n = src.len().min(max_elems);
72 match dtype {
73 DType::F64 => unsafe {
74 // F64 slots are 8 B/elem; widen the f32 input. (Values carry
75 // f32 precision — this is the f32 entry path; `run_typed`'s
76 // `all_f64` branch is the full-precision route.)
77 let dst = dst_ptr as *mut f64;
78 for i in 0..n {
79 *dst.add(i) = src[i] as f64;
80 }
81 },
82 DType::F16 => unsafe {
83 let dst = dst_ptr as *mut half::f16;
84 for i in 0..n {
85 *dst.add(i) = half::f16::from_f32(src[i]);
86 }
87 },
88 DType::BF16 => unsafe {
89 let dst = dst_ptr as *mut half::bf16;
90 for i in 0..n {
91 *dst.add(i) = half::bf16::from_f32(src[i]);
92 }
93 },
94 DType::C64 => unsafe {
95 // Interleaved [re, im, re, im, ...]; `max_elems` is complex count.
96 let dst = dst_ptr as *mut f32;
97 let n = src.len().min(max_elems.saturating_mul(2));
98 std::ptr::copy_nonoverlapping(src.as_ptr(), dst, n);
99 },
100 _ => unsafe {
101 let dst = dst_ptr as *mut f32;
102 std::ptr::copy_nonoverlapping(src.as_ptr(), dst, n);
103 },
104 }
105}
106
107/// Helper: read `n_elems` of `dtype` from `src_ptr`, returning `Vec<f32>`.
108pub unsafe fn read_typed_to_f32(src_ptr: *const u8, dtype: DType, n_elems: usize) -> Vec<f32> {
109 match dtype {
110 DType::F64 => {
111 // F64 slots are 8 B/elem; narrow to f32 for the f32 read path.
112 let mut out = Vec::with_capacity(n_elems);
113 unsafe {
114 let src = src_ptr as *const f64;
115 for i in 0..n_elems {
116 out.push(*src.add(i) as f32);
117 }
118 }
119 out
120 }
121 DType::F16 => {
122 let mut out = Vec::with_capacity(n_elems);
123 unsafe {
124 let src = src_ptr as *const half::f16;
125 for i in 0..n_elems {
126 out.push((*src.add(i)).to_f32());
127 }
128 }
129 out
130 }
131 DType::BF16 => {
132 let mut out = Vec::with_capacity(n_elems);
133 unsafe {
134 let src = src_ptr as *const half::bf16;
135 for i in 0..n_elems {
136 out.push((*src.add(i)).to_f32());
137 }
138 }
139 out
140 }
141 DType::C64 => unsafe {
142 // Interleaved [re, im, re, im, ...]; `n_elems` is complex count.
143 let src = src_ptr as *const f32;
144 std::slice::from_raw_parts(src, n_elems.saturating_mul(2)).to_vec()
145 },
146 _ => unsafe {
147 let src = src_ptr as *const f32;
148 std::slice::from_raw_parts(src, n_elems).to_vec()
149 },
150 }
151}