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