Skip to main content

rlx_cpu/thunk/
host_op.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//! Shared one-op host-fallback plumbing (`Op::ScanBackward` / `ScanBackwardXs`
17//! and similar nested-body ops) mirroring [`super::ScanHostDesc`].
18
19use super::scan::{eval_single_op_f32, run_scan_packed_f32};
20use rlx_ir::{Graph, NodeId, Op, Shape};
21
22/// Compiled nested-body op + outer-arena byte layout — the fields formerly
23/// duplicated as `Step::HostOp` / `Thunk::HostOp` across GPU backends.
24#[derive(Clone, Debug)]
25pub struct HostOpDesc {
26    pub op: Op,
27    pub out_byte_off: usize,
28    pub out_shape: Shape,
29    /// `(byte_offset, declared_shape)` per operand, in graph-input order.
30    pub inputs: Vec<(usize, Shape)>,
31}
32
33/// Build a [`HostOpDesc`] from a graph node. `byte_off` returns the outer-arena
34/// byte offset of a [`NodeId`] (operands and output).
35pub fn host_op_desc_from_node(
36    graph: &Graph,
37    node: &rlx_ir::Node,
38    mut byte_off: impl FnMut(NodeId) -> usize,
39) -> HostOpDesc {
40    HostOpDesc {
41        op: node.op.clone(),
42        out_byte_off: byte_off(node.id),
43        out_shape: node.shape.clone(),
44        inputs: node
45            .inputs
46            .iter()
47            .map(|&id| (byte_off(id), graph.node(id).shape.clone()))
48            .collect(),
49    }
50}
51
52/// Run [`eval_single_op_f32`] against a raw byte arena from a [`HostOpDesc`].
53///
54/// # Safety
55/// `base` must span every outer offset referenced by `desc` (inputs + output).
56pub unsafe fn execute_host_op_on_bytes(base: *mut u8, desc: &HostOpDesc) {
57    let staged: Vec<(Shape, Vec<f32>)> = desc
58        .inputs
59        .iter()
60        .map(|(off, sh)| {
61            let n = sh.num_elements().unwrap_or(0);
62            let mut v = vec![0f32; n];
63            unsafe {
64                let src = base.add(*off) as *const f32;
65                std::ptr::copy_nonoverlapping(src, v.as_mut_ptr(), n);
66            }
67            (sh.clone(), v)
68        })
69        .collect();
70    let refs: Vec<(Shape, &[f32])> = staged
71        .iter()
72        .map(|(sh, v)| (sh.clone(), v.as_slice()))
73        .collect();
74    let y = eval_single_op_f32(&desc.op, &desc.out_shape, &refs);
75    unsafe {
76        let dst = base.add(desc.out_byte_off) as *mut f32;
77        std::ptr::copy_nonoverlapping(y.as_ptr(), dst, y.len());
78    }
79}
80
81/// Evaluate `desc` against a host f32 arena (byte offsets → `/4` element indices).
82/// Used by CUDA / ROCm after a full-arena D2H.
83pub fn eval_host_op_on_f32_arena(host: &mut [f32], desc: &HostOpDesc) {
84    debug_assert!(desc.out_byte_off.is_multiple_of(4));
85    let staged: Vec<(Shape, Vec<f32>)> = desc
86        .inputs
87        .iter()
88        .map(|(off, sh)| {
89            debug_assert!(off.is_multiple_of(4));
90            let off_f32 = *off / 4;
91            let n = sh.num_elements().unwrap_or(0);
92            let end = (off_f32 + n).min(host.len());
93            (sh.clone(), host[off_f32..end].to_vec())
94        })
95        .collect();
96    let refs: Vec<(Shape, &[f32])> = staged
97        .iter()
98        .map(|(sh, v)| (sh.clone(), v.as_slice()))
99        .collect();
100    let y = eval_single_op_f32(&desc.op, &desc.out_shape, &refs);
101    let out_f32 = desc.out_byte_off / 4;
102    let end = (out_f32 + y.len()).min(host.len());
103    host[out_f32..end].copy_from_slice(&y[..(end - out_f32)]);
104}
105
106/// Value-map path: load `Op::Scan` inputs via `get` and run the packed host loop.
107pub fn run_scan_node_f32(node: &rlx_ir::Node, mut get: impl FnMut(NodeId) -> Vec<f32>) -> Vec<f32> {
108    let (body, length, save_trajectory, num_bcast, num_xs) = match &node.op {
109        Op::Scan {
110            body,
111            length,
112            save_trajectory,
113            num_bcast,
114            num_xs,
115            ..
116        } => (
117            body,
118            *length,
119            *save_trajectory,
120            *num_bcast as usize,
121            *num_xs as usize,
122        ),
123        _ => panic!("run_scan_node_f32: expected Op::Scan"),
124    };
125    let init = get(node.inputs[0]);
126    let mut bcasts = Vec::with_capacity(num_bcast);
127    for i in 0..num_bcast {
128        bcasts.push(get(node.inputs[1 + i]));
129    }
130    let mut xs = Vec::with_capacity(num_xs);
131    for i in 0..num_xs {
132        xs.push(get(node.inputs[1 + num_bcast + i]));
133    }
134    let out_len = node.shape.num_elements().unwrap_or(0);
135    run_scan_packed_f32(
136        body,
137        length,
138        save_trajectory,
139        num_bcast,
140        num_xs,
141        &init,
142        &bcasts,
143        &xs,
144        out_len,
145    )
146}
147
148/// Value-map path: load inputs via `get` and evaluate a nested-body op
149/// (`ScanBackward` / `ScanBackwardXs`, …) with [`eval_single_op_f32`].
150pub fn run_host_op_node_f32(
151    graph: &Graph,
152    node: &rlx_ir::Node,
153    mut get: impl FnMut(NodeId) -> Vec<f32>,
154) -> Vec<f32> {
155    let staged: Vec<(Shape, Vec<f32>)> = node
156        .inputs
157        .iter()
158        .map(|&id| (graph.node(id).shape.clone(), get(id)))
159        .collect();
160    let refs: Vec<(Shape, &[f32])> = staged
161        .iter()
162        .map(|(sh, v)| (sh.clone(), v.as_slice()))
163        .collect();
164    eval_single_op_f32(&node.op, &node.shape, &refs)
165}
166
167/// Build a [`HostOpDesc`] from `(graph, node, |nid| byte_offset)`.
168#[macro_export]
169macro_rules! rlx_host_op_desc {
170    ($graph:expr, $node:expr, $byte_off:expr) => {
171        $crate::thunk::host_op_desc_from_node(&$graph, $node, $byte_off)
172    };
173}
174
175/// Execute a host op against a raw byte arena from a [`HostOpDesc`].
176#[macro_export]
177macro_rules! rlx_execute_host_op_on_bytes {
178    ($base:expr, $desc:expr) => {
179        $crate::thunk::execute_host_op_on_bytes($base, $desc)
180    };
181}
182
183/// D2H → CPU host-op → H2D staging for discrete-GPU f32 arenas.
184///
185/// Byte offsets inside `$desc` are converted via `/4` in
186/// [`eval_host_op_on_f32_arena`].
187#[macro_export]
188macro_rules! rlx_host_op_stage_d2h {
189    (
190        arena_size_bytes = $nbytes:expr,
191        desc = $desc:expr,
192        sync = $sync:block,
193        dtoh = |$host:ident| $dtoh:block,
194        htod = |$host_back:ident| $htod:block $(,)?
195    ) => {
196        $crate::rlx_arena_stage_d2h! {
197            arena_size_bytes = $nbytes,
198            sync = $sync,
199            dtoh = |$host| $dtoh,
200            on_host = |host_mut| {
201                $crate::thunk::eval_host_op_on_f32_arena(host_mut, $desc);
202            },
203            htod = |$host_back| $htod,
204        }
205    };
206}
207
208/// Contiguous host span covering every region a [`HostOpDesc`] touches, with
209/// offsets rebased to the span start (wgpu readback mirror of [`super::ScanHostSpan`]).
210#[derive(Clone)]
211pub struct HostOpSpan {
212    pub lo: usize,
213    pub hi: usize,
214    pub desc: HostOpDesc,
215}
216
217impl HostOpSpan {
218    pub fn from_desc(desc: HostOpDesc) -> Self {
219        let out_n = desc.out_shape.num_elements().unwrap_or(0) * 4;
220        let mut lo = desc.out_byte_off;
221        let mut hi = desc.out_byte_off + out_n;
222        for (off, sh) in &desc.inputs {
223            let n = sh.num_elements().unwrap_or(0) * 4;
224            lo = lo.min(*off);
225            hi = hi.max(*off + n);
226        }
227        let rebated = HostOpDesc {
228            op: desc.op,
229            out_byte_off: desc.out_byte_off - lo,
230            out_shape: desc.out_shape,
231            inputs: desc
232                .inputs
233                .into_iter()
234                .map(|(o, sh)| (o - lo, sh))
235                .collect(),
236        };
237        Self {
238            lo,
239            hi,
240            desc: rebated,
241        }
242    }
243
244    pub fn len(&self) -> usize {
245        self.hi - self.lo
246    }
247
248    pub fn is_empty(&self) -> bool {
249        self.len() == 0
250    }
251}