Skip to main content

rlx_cpu/thunk/
indexing_host.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//! Native ScatterNd / ScatterElements / GatherNd / GatherElements host path
17//! for GPU backends.
18//!
19//! CPU already lowers these to [`Thunk::ScatterNd`] (etc.) and runs
20//! [`crate::onnx_indexing`] in-place. GPU backends previously wrapped them in
21//! [`super::HostOpDesc`], which rebuilds a mini-graph per call and stages every
22//! input as `f32` — wrong for `I64` indices and expensive for F5-style graphs
23//! with dozens of scatters per step.
24//!
25//! This module exposes the same native thunks with outer-arena byte offsets so
26//! Metal can run them on unified memory and wgpu/CUDA can stage only the
27//! touched span.
28
29use super::Thunk;
30use super::ops::{exec_gather_elements, exec_gather_nd, exec_scatter_elements, exec_scatter_nd};
31use rlx_ir::{Graph, NodeId, Op};
32
33fn shape_u32s(shape: &rlx_ir::Shape) -> Vec<u32> {
34    (0..shape.rank())
35        .map(|i| shape.dim(i).unwrap_static() as u32)
36        .collect()
37}
38
39/// Build a native indexing [`Thunk`] from a graph node + outer-arena offsets.
40pub fn indexing_thunk_from_node(
41    graph: &Graph,
42    node: &rlx_ir::Node,
43    mut byte_off: impl FnMut(NodeId) -> usize,
44) -> Thunk {
45    match &node.op {
46        Op::ScatterNd { reduction } => {
47            let data_shape = &graph.node(node.inputs[0]).shape;
48            let indices_shape = &graph.node(node.inputs[1]).shape;
49            let updates_shape = &graph.node(node.inputs[2]).shape;
50            Thunk::ScatterNd {
51                data: byte_off(node.inputs[0]),
52                indices: byte_off(node.inputs[1]),
53                updates: byte_off(node.inputs[2]),
54                dst: byte_off(node.id),
55                data_shape: shape_u32s(data_shape),
56                indices_shape: shape_u32s(indices_shape),
57                data_len: data_shape.num_elements().unwrap_or(0) as u32,
58                updates_len: updates_shape.num_elements().unwrap_or(0) as u32,
59                indices_len: indices_shape.num_elements().unwrap_or(0) as u32,
60                indices_i64: u8::from(indices_shape.dtype() == rlx_ir::DType::I64),
61                reduction: *reduction,
62            }
63        }
64        Op::ScatterElements { axis, reduction } => {
65            let data_shape = &graph.node(node.inputs[0]).shape;
66            let indices_shape = &graph.node(node.inputs[1]).shape;
67            let updates_shape = &graph.node(node.inputs[2]).shape;
68            Thunk::ScatterElements {
69                data: byte_off(node.inputs[0]),
70                indices: byte_off(node.inputs[1]),
71                updates: byte_off(node.inputs[2]),
72                dst: byte_off(node.id),
73                data_shape: shape_u32s(data_shape),
74                data_len: data_shape.num_elements().unwrap_or(0) as u32,
75                updates_len: updates_shape.num_elements().unwrap_or(0) as u32,
76                indices_len: indices_shape.num_elements().unwrap_or(0) as u32,
77                indices_i64: u8::from(indices_shape.dtype() == rlx_ir::DType::I64),
78                axis: *axis,
79                reduction: *reduction,
80            }
81        }
82        Op::GatherNd { batch_dims } => {
83            let data_shape = &graph.node(node.inputs[0]).shape;
84            let indices_shape = &graph.node(node.inputs[1]).shape;
85            Thunk::GatherNd {
86                data: byte_off(node.inputs[0]),
87                indices: byte_off(node.inputs[1]),
88                dst: byte_off(node.id),
89                data_shape: shape_u32s(data_shape),
90                indices_shape: shape_u32s(indices_shape),
91                data_len: data_shape.num_elements().unwrap_or(0) as u32,
92                indices_len: indices_shape.num_elements().unwrap_or(0) as u32,
93                out_len: node.shape.num_elements().unwrap_or(0) as u32,
94                indices_i64: u8::from(indices_shape.dtype() == rlx_ir::DType::I64),
95                batch_dims: *batch_dims,
96            }
97        }
98        Op::GatherElements { axis } => {
99            let data_shape = &graph.node(node.inputs[0]).shape;
100            let indices_shape = &graph.node(node.inputs[1]).shape;
101            Thunk::GatherElements {
102                data: byte_off(node.inputs[0]),
103                indices: byte_off(node.inputs[1]),
104                dst: byte_off(node.id),
105                data_shape: shape_u32s(data_shape),
106                indices_shape: shape_u32s(indices_shape),
107                data_len: data_shape.num_elements().unwrap_or(0) as u32,
108                indices_len: indices_shape.num_elements().unwrap_or(0) as u32,
109                out_len: node.shape.num_elements().unwrap_or(0) as u32,
110                indices_i64: u8::from(indices_shape.dtype() == rlx_ir::DType::I64),
111                axis: *axis,
112            }
113        }
114        _ => panic!(
115            "indexing_thunk_from_node: expected ScatterNd/ScatterElements/GatherNd/GatherElements"
116        ),
117    }
118}
119
120fn idx_nbytes(len: u32, indices_i64: u8) -> usize {
121    if indices_i64 != 0 {
122        len as usize * 8
123    } else {
124        len as usize * 4
125    }
126}
127
128/// `(byte_offset, nbytes)` regions touched by an indexing thunk.
129pub fn indexing_thunk_regions(thunk: &Thunk) -> Vec<(usize, usize)> {
130    match thunk {
131        Thunk::ScatterNd {
132            data,
133            indices,
134            updates,
135            dst,
136            data_len,
137            updates_len,
138            indices_len,
139            indices_i64,
140            ..
141        } => vec![
142            (*data, *data_len as usize * 4),
143            (*indices, idx_nbytes(*indices_len, *indices_i64)),
144            (*updates, *updates_len as usize * 4),
145            (*dst, *data_len as usize * 4),
146        ],
147        Thunk::ScatterElements {
148            data,
149            indices,
150            updates,
151            dst,
152            data_len,
153            updates_len,
154            indices_len,
155            indices_i64,
156            ..
157        } => vec![
158            (*data, *data_len as usize * 4),
159            (*indices, idx_nbytes(*indices_len, *indices_i64)),
160            (*updates, *updates_len as usize * 4),
161            (*dst, *data_len as usize * 4),
162        ],
163        Thunk::GatherNd {
164            data,
165            indices,
166            dst,
167            data_len,
168            indices_len,
169            out_len,
170            indices_i64,
171            ..
172        } => vec![
173            (*data, *data_len as usize * 4),
174            (*indices, idx_nbytes(*indices_len, *indices_i64)),
175            (*dst, *out_len as usize * 4),
176        ],
177        Thunk::GatherElements {
178            data,
179            indices,
180            dst,
181            data_len,
182            indices_len,
183            out_len,
184            indices_i64,
185            ..
186        } => vec![
187            (*data, *data_len as usize * 4),
188            (*indices, idx_nbytes(*indices_len, *indices_i64)),
189            (*dst, *out_len as usize * 4),
190        ],
191        _ => panic!("indexing_thunk_regions: not an indexing thunk"),
192    }
193}
194
195fn rebase_off(off: usize, lo: usize) -> usize {
196    off.checked_sub(lo)
197        .expect("indexing host span: offset below span lo")
198}
199
200fn rebase_indexing_thunk(thunk: Thunk, lo: usize) -> Thunk {
201    match thunk {
202        Thunk::ScatterNd {
203            data,
204            indices,
205            updates,
206            dst,
207            data_shape,
208            indices_shape,
209            data_len,
210            updates_len,
211            indices_len,
212            indices_i64,
213            reduction,
214        } => Thunk::ScatterNd {
215            data: rebase_off(data, lo),
216            indices: rebase_off(indices, lo),
217            updates: rebase_off(updates, lo),
218            dst: rebase_off(dst, lo),
219            data_shape,
220            indices_shape,
221            data_len,
222            updates_len,
223            indices_len,
224            indices_i64,
225            reduction,
226        },
227        Thunk::ScatterElements {
228            data,
229            indices,
230            updates,
231            dst,
232            data_shape,
233            data_len,
234            updates_len,
235            indices_len,
236            indices_i64,
237            axis,
238            reduction,
239        } => Thunk::ScatterElements {
240            data: rebase_off(data, lo),
241            indices: rebase_off(indices, lo),
242            updates: rebase_off(updates, lo),
243            dst: rebase_off(dst, lo),
244            data_shape,
245            data_len,
246            updates_len,
247            indices_len,
248            indices_i64,
249            axis,
250            reduction,
251        },
252        Thunk::GatherNd {
253            data,
254            indices,
255            dst,
256            data_shape,
257            indices_shape,
258            data_len,
259            indices_len,
260            out_len,
261            indices_i64,
262            batch_dims,
263        } => Thunk::GatherNd {
264            data: rebase_off(data, lo),
265            indices: rebase_off(indices, lo),
266            dst: rebase_off(dst, lo),
267            data_shape,
268            indices_shape,
269            data_len,
270            indices_len,
271            out_len,
272            indices_i64,
273            batch_dims,
274        },
275        Thunk::GatherElements {
276            data,
277            indices,
278            dst,
279            data_shape,
280            indices_shape,
281            data_len,
282            indices_len,
283            out_len,
284            indices_i64,
285            axis,
286        } => Thunk::GatherElements {
287            data: rebase_off(data, lo),
288            indices: rebase_off(indices, lo),
289            dst: rebase_off(dst, lo),
290            data_shape,
291            indices_shape,
292            data_len,
293            indices_len,
294            out_len,
295            indices_i64,
296            axis,
297        },
298        _ => panic!("rebase_indexing_thunk: not an indexing thunk"),
299    }
300}
301
302/// Contiguous host span covering every region an indexing thunk touches, with
303/// offsets rebased to the span start (wgpu/CUDA readback mirror of
304/// [`super::HostOpSpan`]).
305#[derive(Clone)]
306pub struct IndexingHostSpan {
307    pub lo: usize,
308    pub hi: usize,
309    pub thunk: Thunk,
310}
311
312impl IndexingHostSpan {
313    pub fn from_thunk(thunk: Thunk) -> Self {
314        let regions = indexing_thunk_regions(&thunk);
315        let mut lo = regions[0].0;
316        let mut hi = regions[0].0 + regions[0].1;
317        for &(off, n) in &regions[1..] {
318            lo = lo.min(off);
319            hi = hi.max(off + n);
320        }
321        Self {
322            lo,
323            hi,
324            thunk: rebase_indexing_thunk(thunk, lo),
325        }
326    }
327
328    pub fn len(&self) -> usize {
329        self.hi - self.lo
330    }
331
332    pub fn is_empty(&self) -> bool {
333        self.len() == 0
334    }
335}
336
337/// Destination `(byte_off, nbytes)` written by an indexing thunk.
338pub fn indexing_thunk_dst_region(thunk: &Thunk) -> (usize, usize) {
339    match thunk {
340        Thunk::ScatterNd { dst, data_len, .. } | Thunk::ScatterElements { dst, data_len, .. } => {
341            (*dst, *data_len as usize * 4)
342        }
343        Thunk::GatherNd { dst, out_len, .. } | Thunk::GatherElements { dst, out_len, .. } => {
344            (*dst, *out_len as usize * 4)
345        }
346        _ => panic!("indexing_thunk_dst_region: not an indexing thunk"),
347    }
348}
349
350/// Remap outer-arena byte offsets in an indexing thunk (packed host staging).
351pub fn remap_indexing_thunk_offsets(thunk: Thunk, mut map: impl FnMut(usize) -> usize) -> Thunk {
352    match thunk {
353        Thunk::ScatterNd {
354            data,
355            indices,
356            updates,
357            dst,
358            data_shape,
359            indices_shape,
360            data_len,
361            updates_len,
362            indices_len,
363            indices_i64,
364            reduction,
365        } => Thunk::ScatterNd {
366            data: map(data),
367            indices: map(indices),
368            updates: map(updates),
369            dst: map(dst),
370            data_shape,
371            indices_shape,
372            data_len,
373            updates_len,
374            indices_len,
375            indices_i64,
376            reduction,
377        },
378        Thunk::ScatterElements {
379            data,
380            indices,
381            updates,
382            dst,
383            data_shape,
384            data_len,
385            updates_len,
386            indices_len,
387            indices_i64,
388            axis,
389            reduction,
390        } => Thunk::ScatterElements {
391            data: map(data),
392            indices: map(indices),
393            updates: map(updates),
394            dst: map(dst),
395            data_shape,
396            data_len,
397            updates_len,
398            indices_len,
399            indices_i64,
400            axis,
401            reduction,
402        },
403        Thunk::GatherNd {
404            data,
405            indices,
406            dst,
407            data_shape,
408            indices_shape,
409            data_len,
410            indices_len,
411            out_len,
412            indices_i64,
413            batch_dims,
414        } => Thunk::GatherNd {
415            data: map(data),
416            indices: map(indices),
417            dst: map(dst),
418            data_shape,
419            indices_shape,
420            data_len,
421            indices_len,
422            out_len,
423            indices_i64,
424            batch_dims,
425        },
426        Thunk::GatherElements {
427            data,
428            indices,
429            dst,
430            data_shape,
431            indices_shape,
432            data_len,
433            indices_len,
434            out_len,
435            indices_i64,
436            axis,
437        } => Thunk::GatherElements {
438            data: map(data),
439            indices: map(indices),
440            dst: map(dst),
441            data_shape,
442            indices_shape,
443            data_len,
444            indices_len,
445            out_len,
446            indices_i64,
447            axis,
448        },
449        _ => panic!("remap_indexing_thunk_offsets: not an indexing thunk"),
450    }
451}
452
453/// Max contiguous span (bytes) before discrete GPUs switch to packed region staging.
454///
455/// F5 DiT ScatterNd often places `data` near arena start and `dst` near the end
456/// — a contiguous mirror would be multi-GiB on CUDA/wgpu; packing keeps PCIe
457/// traffic proportional to the touched tensors only.
458pub const INDEXING_CONTIGUOUS_SPAN_CAP: usize = 512 * 1024 * 1024;
459
460/// Run a native indexing thunk against a raw byte arena.
461///
462/// # Safety
463/// `base` must span every outer offset referenced by `thunk` (inputs + output),
464/// including full `I64` index buffers when `indices_i64 != 0`.
465pub unsafe fn execute_indexing_thunk_on_bytes(base: *mut u8, thunk: &Thunk) {
466    match thunk {
467        Thunk::ScatterNd { .. } => exec_scatter_nd(thunk, base),
468        Thunk::ScatterElements { .. } => exec_scatter_elements(thunk, base),
469        Thunk::GatherNd { .. } => exec_gather_nd(thunk, base),
470        Thunk::GatherElements { .. } => exec_gather_elements(thunk, base),
471        _ => panic!("execute_indexing_thunk_on_bytes: not an indexing thunk"),
472    }
473}
474
475/// Opaque wrapper so GPU backends can store a native indexing [`Thunk`] in
476/// `Debug`-derived enums without requiring `Thunk: Debug`.
477#[derive(Clone)]
478pub struct IndexingThunk(pub Thunk);
479
480impl std::fmt::Debug for IndexingThunk {
481    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482        f.write_str("IndexingThunk(..)")
483    }
484}
485
486impl IndexingThunk {
487    pub fn from_node(
488        graph: &Graph,
489        node: &rlx_ir::Node,
490        byte_off: impl FnMut(NodeId) -> usize,
491    ) -> Self {
492        Self(indexing_thunk_from_node(graph, node, byte_off))
493    }
494
495    pub fn inner(&self) -> &Thunk {
496        &self.0
497    }
498
499    /// For f32-uniform GPU arenas (CUDA / wgpu / ROCm): I64 index tensors are
500    /// stored as f32 slots. Force the f32→i64 reader so ScatterNd does not
501    /// reinterpret float bits as i64 (that bug drifted F5 DiT vs CPU/Metal).
502    pub fn force_indices_f32(mut self) -> Self {
503        self.0 = force_indexing_indices_f32(self.0);
504        self
505    }
506}
507
508/// See [`IndexingThunk::force_indices_f32`].
509pub fn force_indexing_indices_f32(thunk: Thunk) -> Thunk {
510    match thunk {
511        Thunk::ScatterNd {
512            data,
513            indices,
514            updates,
515            dst,
516            data_shape,
517            indices_shape,
518            data_len,
519            updates_len,
520            indices_len,
521            reduction,
522            ..
523        } => Thunk::ScatterNd {
524            data,
525            indices,
526            updates,
527            dst,
528            data_shape,
529            indices_shape,
530            data_len,
531            updates_len,
532            indices_len,
533            indices_i64: 0,
534            reduction,
535        },
536        Thunk::ScatterElements {
537            data,
538            indices,
539            updates,
540            dst,
541            data_shape,
542            data_len,
543            updates_len,
544            indices_len,
545            axis,
546            reduction,
547            ..
548        } => Thunk::ScatterElements {
549            data,
550            indices,
551            updates,
552            dst,
553            data_shape,
554            data_len,
555            updates_len,
556            indices_len,
557            indices_i64: 0,
558            axis,
559            reduction,
560        },
561        Thunk::GatherNd {
562            data,
563            indices,
564            dst,
565            data_shape,
566            indices_shape,
567            data_len,
568            indices_len,
569            out_len,
570            batch_dims,
571            ..
572        } => Thunk::GatherNd {
573            data,
574            indices,
575            dst,
576            data_shape,
577            indices_shape,
578            data_len,
579            indices_len,
580            out_len,
581            indices_i64: 0,
582            batch_dims,
583        },
584        Thunk::GatherElements {
585            data,
586            indices,
587            dst,
588            data_shape,
589            indices_shape,
590            data_len,
591            indices_len,
592            out_len,
593            axis,
594            ..
595        } => Thunk::GatherElements {
596            data,
597            indices,
598            dst,
599            data_shape,
600            indices_shape,
601            data_len,
602            indices_len,
603            out_len,
604            indices_i64: 0,
605            axis,
606        },
607        other => other,
608    }
609}
610
611/// Build an indexing thunk from `(graph, node, |nid| byte_offset)`.
612#[macro_export]
613macro_rules! rlx_indexing_thunk {
614    ($graph:expr, $node:expr, $byte_off:expr) => {
615        $crate::thunk::IndexingThunk::from_node(&$graph, $node, $byte_off)
616    };
617}
618
619/// Execute a native indexing thunk against a raw byte arena.
620#[macro_export]
621macro_rules! rlx_execute_indexing_on_bytes {
622    ($base:expr, $thunk:expr) => {
623        $crate::thunk::execute_indexing_thunk_on_bytes($base, $thunk)
624    };
625}
626
627/// D2H → native indexing thunk → H2D staging for discrete-GPU f32 arenas.
628///
629/// Runs against the full arena with original byte offsets (I64 index buffers
630/// stay intact in the byte view of the f32 host mirror).
631#[macro_export]
632macro_rules! rlx_indexing_stage_d2h {
633    (
634        arena_size_bytes = $nbytes:expr,
635        thunk = $thunk:expr,
636        sync = $sync:block,
637        dtoh = |$host:ident| $dtoh:block,
638        htod = |$host_back:ident| $htod:block $(,)?
639    ) => {
640        $crate::rlx_arena_stage_d2h! {
641            arena_size_bytes = $nbytes,
642            sync = $sync,
643            dtoh = |$host| $dtoh,
644            on_host = |host_mut| {
645                unsafe {
646                    $crate::thunk::execute_indexing_thunk_on_bytes(
647                        host_mut.as_mut_ptr() as *mut u8,
648                        $thunk,
649                    );
650                }
651            },
652            htod = |$host_back| $htod,
653        }
654    };
655}