Skip to main content

rlx_cpu/thunk/ops/
elementwise.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#![allow(unsafe_op_in_unsafe_fn)]
5use crate::thunk::*;
6
7#[allow(unused_variables)]
8pub(crate) fn compile_gather(
9    node: &rlx_ir::Node,
10    graph: &Graph,
11    arena: &crate::arena::Arena,
12    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
13    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
14    rng: rlx_ir::RngOptions,
15) -> Thunk {
16    let Op::Gather { axis } = &node.op else {
17        unreachable!()
18    };
19    {
20        // Non-zero axis: outer × num_idx × trailing layout.
21        let table_shape = &graph.node(node.inputs[0]).shape;
22        let rank = table_shape.rank();
23        let outer: usize = (0..*axis)
24            .map(|i| table_shape.dim(i).unwrap_static())
25            .product::<usize>()
26            .max(1);
27        let trailing: usize = (*axis + 1..rank)
28            .map(|i| table_shape.dim(i).unwrap_static())
29            .product::<usize>()
30            .max(1);
31        let axis_dim = table_shape.dim(*axis).unwrap_static();
32        let idx_len = get_len(graph, node.inputs[1]);
33        let idx_i64 = u8::from(graph.node(node.inputs[1]).shape.dtype() == rlx_ir::DType::I64);
34        let table_bytes = graph.node(node.inputs[0]).shape.dtype().size_bytes() as u8;
35        Thunk::GatherAxis {
36            table: node_offset(arena, node.inputs[0]),
37            idx: node_offset(arena, node.inputs[1]),
38            dst: node_offset(arena, node.id),
39            outer: outer as u32,
40            axis_dim: axis_dim as u32,
41            num_idx: idx_len as u32,
42            trailing: trailing as u32,
43            idx_i64,
44            table_bytes,
45        }
46    }
47}
48
49#[allow(unused_variables)]
50pub(crate) fn compile_narrow(
51    node: &rlx_ir::Node,
52    graph: &Graph,
53    arena: &crate::arena::Arena,
54    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
55    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
56    rng: rlx_ir::RngOptions,
57) -> Thunk {
58    let Op::Narrow { axis, start, len } = &node.op else {
59        unreachable!()
60    };
61    {
62        let in_shape = &graph.node(node.inputs[0]).shape;
63        let elem_bytes = in_shape.dtype().size_bytes() as u8;
64        let rank = in_shape.rank();
65        let outer: usize = (0..*axis)
66            .map(|i| in_shape.dim(i).unwrap_static())
67            .product::<usize>()
68            .max(1);
69        let inner: usize = (*axis + 1..rank)
70            .map(|i| in_shape.dim(i).unwrap_static())
71            .product::<usize>()
72            .max(1);
73        let in_axis = in_shape.dim(*axis).unwrap_static();
74        let src_byte_offset =
75            node_offset(arena, node.inputs[0]) + start * inner * elem_bytes as usize;
76        Thunk::Narrow {
77            src: src_byte_offset,
78            dst: node_offset(arena, node.id),
79            outer: outer as u32,
80            src_stride: (in_axis * inner) as u32, // elements per outer step in source
81            dst_stride: (*len * inner) as u32,    // elements per outer step in dest
82            inner: (*len * inner) as u32,         // elements to copy per outer step
83            elem_bytes,
84        }
85    }
86}
87
88#[allow(unused_variables)]
89pub(crate) fn compile_reverse(
90    node: &rlx_ir::Node,
91    graph: &Graph,
92    arena: &crate::arena::Arena,
93    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
94    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
95    rng: rlx_ir::RngOptions,
96) -> Thunk {
97    let Op::Reverse { axes } = &node.op else {
98        unreachable!()
99    };
100    {
101        let in_shape = &graph.node(node.inputs[0]).shape;
102        let rank = in_shape.rank();
103        let dims: Vec<u32> = (0..rank)
104            .map(|i| in_shape.dim(i).unwrap_static() as u32)
105            .collect();
106        let mut rev_mask = vec![false; rank];
107        for &a in axes {
108            if a < rank {
109                rev_mask[a] = true;
110            }
111        }
112        Thunk::Reverse {
113            src: node_offset(arena, node.inputs[0]),
114            dst: node_offset(arena, node.id),
115            dims,
116            rev_mask,
117            elem_bytes: in_shape.dtype().size_bytes() as u8,
118        }
119    }
120}
121
122#[allow(unused_variables)]
123pub(crate) fn compile_cast(
124    node: &rlx_ir::Node,
125    graph: &Graph,
126    arena: &crate::arena::Arena,
127    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
128    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
129    rng: rlx_ir::RngOptions,
130) -> Thunk {
131    let Op::Cast { to } = &node.op else {
132        unreachable!()
133    };
134    {
135        let in_node = graph.node(node.inputs[0]);
136        let in_dtype = in_node.shape.dtype();
137        let out_dtype = *to;
138        let len = node.shape.num_elements().unwrap();
139        let src = node_offset(arena, node.inputs[0]);
140        let dst = node_offset(arena, node.id);
141        if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::I64 {
142            Thunk::CastF32ToI64 {
143                src,
144                dst,
145                len: len as u32,
146            }
147        } else if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::F64 {
148            Thunk::CastF32ToF64 {
149                src,
150                dst,
151                len: len as u32,
152            }
153        } else if in_dtype == rlx_ir::DType::F64 && out_dtype == rlx_ir::DType::F32 {
154            // f64 → f32 narrowing. Input is 8 bytes/element; the generic Copy
155            // below would misread the f64 buffer as 4-byte f32 lanes (two
156            // garbage f32s per f64).
157            Thunk::CastF64ToF32 {
158                src,
159                dst,
160                len: len as u32,
161            }
162        } else if in_dtype == rlx_ir::DType::F64 && out_dtype == rlx_ir::DType::I64 {
163            // f64 → i64, truncating toward zero. Generic Copy bit-copies.
164            Thunk::CastF64ToI64 {
165                src,
166                dst,
167                len: len as u32,
168            }
169        } else if in_dtype == rlx_ir::DType::F64 && out_dtype == rlx_ir::DType::I32 {
170            // f64 → i32, truncating toward zero. 8-byte src → 4-byte dst.
171            Thunk::CastF64ToI32 {
172                src,
173                dst,
174                len: len as u32,
175            }
176        } else if in_dtype == rlx_ir::DType::F64 && out_dtype == rlx_ir::DType::Bool {
177            // f64 → Bool (`x != 0.0`). 8-byte src → 1-byte dst.
178            Thunk::CastF64ToBool {
179                src,
180                dst,
181                len: len as u32,
182            }
183        } else if in_dtype == rlx_ir::DType::I64 && out_dtype == rlx_ir::DType::I32 {
184            // i64 → i32 narrowing (mirror of the CastI32ToI64 widening below).
185            // 8-byte src → 4-byte dst; generic Copy keeps only low 4 bytes.
186            Thunk::CastI64ToI32 {
187                src,
188                dst,
189                len: len as u32,
190            }
191        } else if in_dtype == rlx_ir::DType::I64 && out_dtype == rlx_ir::DType::F64 {
192            // i64 → f64. Both 8 bytes; generic Copy moves 4 and bit-reinterprets.
193            Thunk::CastI64ToF64 {
194                src,
195                dst,
196                len: len as u32,
197            }
198        } else if in_dtype == rlx_ir::DType::I32 && out_dtype == rlx_ir::DType::F64 {
199            // i32 → f64 widening (4-byte src → 8-byte dst).
200            Thunk::CastI32ToF64 {
201                src,
202                dst,
203                len: len as u32,
204            }
205        } else if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::F16 {
206            // f32 → f16 narrowing (4-byte src → 2-byte f16 dst).
207            Thunk::CastF32ToF16 {
208                src,
209                dst,
210                len: len as u32,
211            }
212        } else if in_dtype == rlx_ir::DType::F16 && out_dtype == rlx_ir::DType::F32 {
213            // f16 → f32 widening (2-byte src → 4-byte dst).
214            Thunk::CastF16ToF32 {
215                src,
216                dst,
217                len: len as u32,
218            }
219        } else if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::BF16 {
220            // f32 → bf16 narrowing (4-byte src → 2-byte bf16 dst).
221            Thunk::CastF32ToBf16 {
222                src,
223                dst,
224                len: len as u32,
225            }
226        } else if in_dtype == rlx_ir::DType::BF16 && out_dtype == rlx_ir::DType::F32 {
227            // bf16 → f32 widening (2-byte src → 4-byte dst).
228            Thunk::CastBf16ToF32 {
229                src,
230                dst,
231                len: len as u32,
232            }
233        } else if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::I32 {
234            Thunk::CastF32ToI32 {
235                src,
236                dst,
237                len: len as u32,
238            }
239        } else if in_dtype == rlx_ir::DType::I64 && out_dtype == rlx_ir::DType::F32 {
240            Thunk::CastI64ToF32 {
241                src,
242                dst,
243                len: len as u32,
244            }
245        } else if in_dtype == rlx_ir::DType::Bool && out_dtype == rlx_ir::DType::I32 {
246            Thunk::CastBoolToI32 {
247                src,
248                dst,
249                len: len as u32,
250            }
251        } else if in_dtype == rlx_ir::DType::Bool && out_dtype == rlx_ir::DType::F32 {
252            // Bool is 1 byte; the generic f32 Copy below would misread it as
253            // 4-byte f32. VITS sequence masks are `Cast(Less(...), f32)`.
254            Thunk::CastBoolToF32 {
255                src,
256                dst,
257                len: len as u32,
258            }
259        } else if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::Bool {
260            // f32 → 1-byte Bool. Output is native-width Bool (1 byte); the
261            // generic Copy would write 4 bytes. Used by logical And/Or.
262            Thunk::CastF32ToBool {
263                src,
264                dst,
265                len: len as u32,
266            }
267        } else if in_dtype == rlx_ir::DType::I32 && out_dtype == rlx_ir::DType::F32 {
268            Thunk::CastI32ToF32 {
269                src,
270                dst,
271                len: len as u32,
272            }
273        } else if in_dtype == rlx_ir::DType::I32 && out_dtype == rlx_ir::DType::I64 {
274            Thunk::CastI32ToI64 {
275                src,
276                dst,
277                len: len as u32,
278            }
279        } else if in_dtype == rlx_ir::DType::I32 && out_dtype == rlx_ir::DType::Bool {
280            Thunk::CastI32ToBool {
281                src,
282                dst,
283                len: len as u32,
284            }
285        } else if in_dtype == rlx_ir::DType::I64 && out_dtype == rlx_ir::DType::Bool {
286            Thunk::CastI64ToBool {
287                src,
288                dst,
289                len: len as u32,
290            }
291        } else if in_dtype == rlx_ir::DType::Bool && out_dtype == rlx_ir::DType::I64 {
292            Thunk::CastBoolToI64 {
293                src,
294                dst,
295                len: len as u32,
296            }
297        } else if in_dtype == out_dtype {
298            match out_dtype {
299                rlx_ir::DType::F64 => Thunk::CopyF64 {
300                    src,
301                    dst,
302                    len: len as u32,
303                },
304                rlx_ir::DType::I64 => Thunk::CopyI64 {
305                    src,
306                    dst,
307                    len: len as u32,
308                },
309                // C128 is 16 bytes/element; the generic `Copy` (4-byte
310                // f32 lanes) would move only a quarter of each element.
311                // Route through the element-wise cast kernel — a
312                // C128→C128 decode/write is bit-identical.
313                rlx_ir::DType::C128 => Thunk::CastGeneric {
314                    src,
315                    dst,
316                    len: len as u32,
317                    src_dtype: in_dtype,
318                    dst_dtype: out_dtype,
319                },
320                _ => Thunk::Copy {
321                    src,
322                    dst,
323                    len: len as u32,
324                },
325            }
326        } else {
327            // Any remaining dtype pair (exotic widths I8/I16/U8/U32, F16/BF16
328            // against non-F32, and C64 real↔complex). The generic 4-byte Copy
329            // that used to live here silently corrupted these (wrong element
330            // width and/or a raw bit-reinterpret instead of a numeric convert);
331            // route them through the correct element-by-element kernel instead.
332            Thunk::CastGeneric {
333                src,
334                dst,
335                len: len as u32,
336                src_dtype: in_dtype,
337                dst_dtype: out_dtype,
338            }
339        }
340    }
341}
342
343/// Intermediate scalar for [`exec_cast_generic`]: a source element decoded into
344/// a backend-neutral form, then re-encoded to the destination dtype. Floats
345/// (incl. F16/BF16) decode to `F`; ints/bool to `I` (preserving full integer
346/// precision — the only lossy step is `i64 → f64` when the DEST is a float);
347/// complex to `C(re, im)`.
348#[derive(Clone, Copy)]
349enum CastScalar {
350    F(f64),
351    I(i64),
352    C(f64, f64),
353}
354
355impl CastScalar {
356    /// Real magnitude for a real (float/int) destination — complex takes the
357    /// real part (numpy/torch convention).
358    #[inline]
359    fn real(self) -> f64 {
360        match self {
361            CastScalar::F(f) => f,
362            CastScalar::I(i) => i as f64,
363            CastScalar::C(re, _) => re,
364        }
365    }
366
367    /// Truthiness for a Bool destination. Complex is nonzero if EITHER
368    /// component is nonzero (numpy convention).
369    #[inline]
370    fn truthy(self) -> bool {
371        match self {
372            CastScalar::F(f) => f != 0.0,
373            CastScalar::I(i) => i != 0,
374            CastScalar::C(re, im) => re != 0.0 || im != 0.0,
375        }
376    }
377
378    /// `(re, im)` for a C64 (f32-component) destination. Non-complex sources
379    /// map to `(value, 0.0)`.
380    #[inline]
381    fn complex(self) -> (f32, f32) {
382        match self {
383            CastScalar::F(f) => (f as f32, 0.0),
384            CastScalar::I(i) => (i as f32, 0.0),
385            CastScalar::C(re, im) => (re as f32, im as f32),
386        }
387    }
388
389    /// `(re, im)` for a C128 (f64-component) destination. Non-complex sources
390    /// map to `(value, 0.0)`; C64/C128 sources keep full f64 precision (a
391    /// C64→C128 widen is exact, a C128→C128 copy is bit-identical).
392    #[inline]
393    fn complex_f64(self) -> (f64, f64) {
394        match self {
395            CastScalar::F(f) => (f, 0.0),
396            CastScalar::I(i) => (i as f64, 0.0),
397            CastScalar::C(re, im) => (re, im),
398        }
399    }
400}
401
402/// Generic element-wise scalar cast for any dtype pair (see
403/// [`Thunk::CastGeneric`]). Decodes each source element into a [`CastScalar`],
404/// then re-encodes into the destination dtype with correct numeric semantics:
405///   * float dest (F16/BF16/F32/F64): value-as-f64 rounded to the target
406///     (round-to-nearest for F16/BF16).
407///   * int dest (I8/I16/I32/I64/U8/U32): from a float use `as` (Rust SATURATES
408///     on overflow — the ONNX/torch behavior); from an int use `as`
409///     (wrapping/narrowing).
410///   * Bool dest: `!= 0` (int) / `!= 0.0` (float) / `re||im != 0` (complex).
411///   * C64 dest: `(value as f32, 0.0)` from a scalar, `(re, im)` from complex.
412///   * C128 dest: same as C64 but f64 components (C64→C128 widens exactly,
413///     C128→C64 narrows f64→f32, C128→C128 is bit-identical).
414///
415/// Reused by the Metal backend (`rlx_cpu::thunk::exec_cast_generic`): Apple
416/// Silicon's unified memory lets the GPU arena pointer be handed straight to
417/// this host kernel, so every `Op::Cast` dtype pair converts with identical
418/// numeric semantics on CPU and Metal — hence `pub`.
419#[allow(clippy::not_unsafe_ptr_arg_deref)] // arena `base`; Metal/CPU share this host kernel
420pub fn exec_cast_generic(
421    src: usize,
422    dst: usize,
423    len: usize,
424    src_dtype: rlx_ir::DType,
425    dst_dtype: rlx_ir::DType,
426    base: *mut u8,
427) {
428    use rlx_ir::DType as DT;
429    if len == 0 {
430        return;
431    }
432    // Decode all source elements into an owned buffer FIRST so the source and
433    // destination arena views never alias during the write pass (they are
434    // disjoint arena buffers, but this also keeps the borrow trivially sound).
435    let mut vals: Vec<CastScalar> = Vec::with_capacity(len);
436    unsafe {
437        match src_dtype {
438            DT::F32 => {
439                let s = sl(src, base, len);
440                for i in 0..len {
441                    vals.push(CastScalar::F(s[i] as f64));
442                }
443            }
444            DT::F64 => {
445                let s = sl_f64(src, base, len);
446                for i in 0..len {
447                    vals.push(CastScalar::F(s[i]));
448                }
449            }
450            DT::F16 => {
451                let s = sl_f16(src, base, len);
452                for i in 0..len {
453                    vals.push(CastScalar::F(s[i].to_f32() as f64));
454                }
455            }
456            DT::BF16 => {
457                let s = sl_bf16(src, base, len);
458                for i in 0..len {
459                    vals.push(CastScalar::F(s[i].to_f32() as f64));
460                }
461            }
462            DT::I8 => {
463                let s = sl_i8(src, base, len);
464                for i in 0..len {
465                    vals.push(CastScalar::I(s[i] as i64));
466                }
467            }
468            DT::I16 => {
469                let s = sl_i16(src, base, len);
470                for i in 0..len {
471                    vals.push(CastScalar::I(s[i] as i64));
472                }
473            }
474            DT::I32 => {
475                let s = sl_i32(src, base, len);
476                for i in 0..len {
477                    vals.push(CastScalar::I(s[i] as i64));
478                }
479            }
480            DT::I64 => {
481                let s = sl_i64(src, base, len);
482                for i in 0..len {
483                    vals.push(CastScalar::I(s[i]));
484                }
485            }
486            DT::U8 => {
487                let s = sl_u8(src, base, len);
488                for i in 0..len {
489                    vals.push(CastScalar::I(s[i] as i64));
490                }
491            }
492            DT::U32 => {
493                let s = sl_u32(src, base, len);
494                for i in 0..len {
495                    vals.push(CastScalar::I(s[i] as i64));
496                }
497            }
498            DT::Bool => {
499                let s = sl_u8(src, base, len);
500                for i in 0..len {
501                    vals.push(CastScalar::I(i64::from(s[i] != 0)));
502                }
503            }
504            DT::C64 => {
505                // Interleaved [re, im] f32 pairs → 2·len f32 lanes.
506                let s = sl(src, base, 2 * len);
507                for i in 0..len {
508                    vals.push(CastScalar::C(s[2 * i] as f64, s[2 * i + 1] as f64));
509                }
510            }
511            DT::C128 => {
512                // Interleaved [re, im] f64 pairs → 2·len f64 lanes.
513                let s = sl_f64(src, base, 2 * len);
514                for i in 0..len {
515                    vals.push(CastScalar::C(s[2 * i], s[2 * i + 1]));
516                }
517            }
518        }
519
520        // Int destinations: from a float source `as` SATURATES (ONNX/torch);
521        // from an int source `as` wraps/narrows. Complex takes the real part.
522        macro_rules! write_int {
523            ($accessor:ident, $t:ty) => {{
524                let o = $accessor(dst, base, len);
525                for i in 0..len {
526                    o[i] = match vals[i] {
527                        CastScalar::I(n) => n as $t,
528                        CastScalar::F(f) => f as $t,
529                        CastScalar::C(re, _) => re as $t,
530                    };
531                }
532            }};
533        }
534        match dst_dtype {
535            DT::F32 => {
536                let o = sl_mut(dst, base, len);
537                for i in 0..len {
538                    o[i] = vals[i].real() as f32;
539                }
540            }
541            DT::F64 => {
542                let o = sl_mut_f64(dst, base, len);
543                for i in 0..len {
544                    o[i] = vals[i].real();
545                }
546            }
547            DT::F16 => {
548                let o = sl_mut_f16(dst, base, len);
549                for i in 0..len {
550                    o[i] = half::f16::from_f32(vals[i].real() as f32);
551                }
552            }
553            DT::BF16 => {
554                let o = sl_mut_bf16(dst, base, len);
555                for i in 0..len {
556                    o[i] = half::bf16::from_f32(vals[i].real() as f32);
557                }
558            }
559            DT::I8 => write_int!(sl_mut_i8, i8),
560            DT::I16 => write_int!(sl_mut_i16, i16),
561            DT::I32 => write_int!(sl_mut_i32, i32),
562            DT::I64 => write_int!(sl_mut_i64, i64),
563            DT::U8 => write_int!(sl_mut_u8, u8),
564            DT::U32 => write_int!(sl_mut_u32, u32),
565            DT::Bool => {
566                let o = sl_mut_u8(dst, base, len);
567                for i in 0..len {
568                    o[i] = u8::from(vals[i].truthy());
569                }
570            }
571            DT::C64 => {
572                // Interleaved [re, im] f32 pairs → 2·len f32 lanes.
573                let o = sl_mut(dst, base, 2 * len);
574                for i in 0..len {
575                    let (re, im) = vals[i].complex();
576                    o[2 * i] = re;
577                    o[2 * i + 1] = im;
578                }
579            }
580            DT::C128 => {
581                // Interleaved [re, im] f64 pairs → 2·len f64 lanes.
582                let o = sl_mut_f64(dst, base, 2 * len);
583                for i in 0..len {
584                    let (re, im) = vals[i].complex_f64();
585                    o[2 * i] = re;
586                    o[2 * i + 1] = im;
587                }
588            }
589        }
590    }
591}
592
593#[allow(unused_variables)]
594pub(crate) fn compile_expand(
595    node: &rlx_ir::Node,
596    graph: &Graph,
597    arena: &crate::arena::Arena,
598    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
599    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
600    rng: rlx_ir::RngOptions,
601) -> Thunk {
602    let Op::Expand { .. } = &node.op else {
603        unreachable!()
604    };
605    {
606        // Broadcast: build per-output-dim strides where any input dim
607        // of size 1 has stride 0 (read the same element repeatedly).
608        // Reuses the Thunk::Transpose runtime — N-D walk with strides
609        // is identical; only the strides differ.
610        let in_shape = &graph.node(node.inputs[0]).shape;
611        let out_shape = &node.shape;
612        let in_rank = in_shape.rank();
613        let out_rank = out_shape.rank();
614        // Implicit leading 1s if input has lower rank.
615        let pad = out_rank.saturating_sub(in_rank);
616        let in_dims: Vec<usize> = (0..out_rank)
617            .map(|i| {
618                if i < pad {
619                    1
620                } else {
621                    in_shape.dim(i - pad).unwrap_static()
622                }
623            })
624            .collect();
625        // Row-major input strides (over the padded shape).
626        let mut in_strides_full = vec![1usize; out_rank];
627        for d in (0..out_rank.saturating_sub(1)).rev() {
628            in_strides_full[d] = in_strides_full[d + 1] * in_dims[d + 1];
629        }
630        let out_dims: Vec<u32> = (0..out_rank)
631            .map(|i| out_shape.dim(i).unwrap_static() as u32)
632            .collect();
633        // Stride is 0 for broadcast dims (in_dim == 1 && out_dim > 1).
634        let in_strides: Vec<u32> = (0..out_rank)
635            .map(|i| {
636                if in_dims[i] == 1 && (out_dims[i] as usize) > 1 {
637                    0
638                } else {
639                    in_strides_full[i] as u32
640                }
641            })
642            .collect();
643        let in_total = in_dims.iter().product::<usize>() as u32;
644        let src = node_offset(arena, node.inputs[0]);
645        let dst = node_offset(arena, node.id);
646        let elem_bytes = node.shape.dtype().size_bytes() as u8;
647        match node.shape.dtype() {
648            rlx_ir::DType::F64 => Thunk::TransposeF64 {
649                src,
650                dst,
651                in_total,
652                out_dims,
653                in_strides,
654            },
655            _ => Thunk::Transpose {
656                src,
657                dst,
658                in_total,
659                out_dims,
660                in_strides,
661                elem_bytes,
662            },
663        }
664    }
665}
666
667#[allow(unused_variables)]
668pub(crate) fn compile_transpose(
669    node: &rlx_ir::Node,
670    graph: &Graph,
671    arena: &crate::arena::Arena,
672    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
673    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
674    rng: rlx_ir::RngOptions,
675) -> Thunk {
676    let Op::Transpose { perm } = &node.op else {
677        unreachable!()
678    };
679    {
680        // Pre-compute (out_dims, in_strides_for_each_out_dim) so the
681        // runtime loop is just an N-D index walk + scatter.
682        let in_shape = &graph.node(node.inputs[0]).shape;
683        let in_rank = in_shape.rank();
684        if perm.iter().any(|&p| p >= in_rank) {
685            Thunk::Nop
686        } else {
687            let in_dims: Vec<usize> = (0..in_rank)
688                .map(|i| in_shape.dim(i).unwrap_static())
689                .collect();
690            // Row-major input strides: stride[d] = product of dims[d+1..].
691            let mut in_strides_full = vec![1usize; in_rank];
692            for d in (0..in_rank.saturating_sub(1)).rev() {
693                in_strides_full[d] = in_strides_full[d + 1] * in_dims[d + 1];
694            }
695            let out_dims: Vec<u32> = perm.iter().map(|&p| in_dims[p] as u32).collect();
696            let in_strides: Vec<u32> = perm.iter().map(|&p| in_strides_full[p] as u32).collect();
697            let in_total = in_dims.iter().product::<usize>() as u32;
698            let src = node_offset(arena, node.inputs[0]);
699            let dst = node_offset(arena, node.id);
700            let elem_bytes = node.shape.dtype().size_bytes() as u8;
701            match node.shape.dtype() {
702                rlx_ir::DType::F64 => Thunk::TransposeF64 {
703                    src,
704                    dst,
705                    in_total,
706                    out_dims,
707                    in_strides,
708                },
709                _ => Thunk::Transpose {
710                    src,
711                    dst,
712                    in_total,
713                    out_dims,
714                    in_strides,
715                    elem_bytes,
716                },
717            }
718        }
719    }
720}
721
722#[allow(unused_variables)]
723pub(crate) fn compile_scatter_add(
724    node: &rlx_ir::Node,
725    graph: &Graph,
726    arena: &crate::arena::Arena,
727    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
728    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
729    rng: rlx_ir::RngOptions,
730) -> Thunk {
731    let Op::ScatterAdd = &node.op else {
732        unreachable!()
733    };
734    {
735        // updates: [num_updates, ...trailing], indices: [num_updates],
736        // output: [out_dim, ...trailing]
737        let upd_shape = &graph.node(node.inputs[0]).shape;
738        let out_shape = &node.shape;
739        let num_updates = upd_shape.dim(0).unwrap_static();
740        let out_dim = out_shape.dim(0).unwrap_static();
741        let trailing: usize = (1..out_shape.rank())
742            .map(|i| out_shape.dim(i).unwrap_static())
743            .product::<usize>()
744            .max(1);
745        Thunk::ScatterAdd {
746            updates: node_offset(arena, node.inputs[0]),
747            indices: node_offset(arena, node.inputs[1]),
748            dst: node_offset(arena, node.id),
749            num_updates: num_updates as u32,
750            out_dim: out_dim as u32,
751            trailing: trailing as u32,
752        }
753    }
754}
755
756#[allow(unused_variables)]
757pub(crate) fn compile_scatter_nd(
758    node: &rlx_ir::Node,
759    graph: &Graph,
760    arena: &crate::arena::Arena,
761    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
762    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
763    rng: rlx_ir::RngOptions,
764) -> Thunk {
765    let Op::ScatterNd { reduction } = &node.op else {
766        unreachable!()
767    };
768    let data_shape = &graph.node(node.inputs[0]).shape;
769    let indices_shape = &graph.node(node.inputs[1]).shape;
770    let updates_shape = &graph.node(node.inputs[2]).shape;
771    let data_len = data_shape.num_elements().unwrap_or(0);
772    let updates_len = updates_shape.num_elements().unwrap_or(0);
773    let indices_len = indices_shape.num_elements().unwrap_or(0);
774    let indices_i64 = u8::from(indices_shape.dtype() == rlx_ir::DType::I64);
775    Thunk::ScatterNd {
776        data: node_offset(arena, node.inputs[0]),
777        indices: node_offset(arena, node.inputs[1]),
778        updates: node_offset(arena, node.inputs[2]),
779        dst: node_offset(arena, node.id),
780        data_shape: (0..data_shape.rank())
781            .map(|i| data_shape.dim(i).unwrap_static() as u32)
782            .collect(),
783        indices_shape: (0..indices_shape.rank())
784            .map(|i| indices_shape.dim(i).unwrap_static() as u32)
785            .collect(),
786        data_len: data_len as u32,
787        updates_len: updates_len as u32,
788        indices_len: indices_len as u32,
789        indices_i64,
790        reduction: *reduction,
791    }
792}
793
794fn shape_u32s(shape: &rlx_ir::Shape) -> Vec<u32> {
795    (0..shape.rank())
796        .map(|i| shape.dim(i).unwrap_static() as u32)
797        .collect()
798}
799
800#[allow(unused_variables)]
801pub(crate) fn compile_scatter_elements(
802    node: &rlx_ir::Node,
803    graph: &Graph,
804    arena: &crate::arena::Arena,
805    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
806    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
807    rng: rlx_ir::RngOptions,
808) -> Thunk {
809    let Op::ScatterElements { axis, reduction } = &node.op else {
810        unreachable!()
811    };
812    let data_shape = &graph.node(node.inputs[0]).shape;
813    let indices_shape = &graph.node(node.inputs[1]).shape;
814    let updates_shape = &graph.node(node.inputs[2]).shape;
815    Thunk::ScatterElements {
816        data: node_offset(arena, node.inputs[0]),
817        indices: node_offset(arena, node.inputs[1]),
818        updates: node_offset(arena, node.inputs[2]),
819        dst: node_offset(arena, node.id),
820        data_shape: shape_u32s(data_shape),
821        data_len: data_shape.num_elements().unwrap_or(0) as u32,
822        updates_len: updates_shape.num_elements().unwrap_or(0) as u32,
823        indices_len: indices_shape.num_elements().unwrap_or(0) as u32,
824        indices_i64: u8::from(indices_shape.dtype() == rlx_ir::DType::I64),
825        axis: *axis,
826        reduction: *reduction,
827    }
828}
829
830#[allow(unused_variables)]
831pub(crate) fn compile_gather_nd(
832    node: &rlx_ir::Node,
833    graph: &Graph,
834    arena: &crate::arena::Arena,
835    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
836    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
837    rng: rlx_ir::RngOptions,
838) -> Thunk {
839    let Op::GatherNd { batch_dims } = &node.op else {
840        unreachable!()
841    };
842    let data_shape = &graph.node(node.inputs[0]).shape;
843    let indices_shape = &graph.node(node.inputs[1]).shape;
844    Thunk::GatherNd {
845        data: node_offset(arena, node.inputs[0]),
846        indices: node_offset(arena, node.inputs[1]),
847        dst: node_offset(arena, node.id),
848        data_shape: shape_u32s(data_shape),
849        indices_shape: shape_u32s(indices_shape),
850        data_len: data_shape.num_elements().unwrap_or(0) as u32,
851        indices_len: indices_shape.num_elements().unwrap_or(0) as u32,
852        out_len: node.shape.num_elements().unwrap_or(0) as u32,
853        indices_i64: u8::from(indices_shape.dtype() == rlx_ir::DType::I64),
854        batch_dims: *batch_dims,
855    }
856}
857
858#[allow(unused_variables)]
859pub(crate) fn compile_gather_elements(
860    node: &rlx_ir::Node,
861    graph: &Graph,
862    arena: &crate::arena::Arena,
863    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
864    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
865    rng: rlx_ir::RngOptions,
866) -> Thunk {
867    let Op::GatherElements { axis } = &node.op else {
868        unreachable!()
869    };
870    let data_shape = &graph.node(node.inputs[0]).shape;
871    let indices_shape = &graph.node(node.inputs[1]).shape;
872    Thunk::GatherElements {
873        data: node_offset(arena, node.inputs[0]),
874        indices: node_offset(arena, node.inputs[1]),
875        dst: node_offset(arena, node.id),
876        data_shape: shape_u32s(data_shape),
877        indices_shape: shape_u32s(indices_shape),
878        data_len: data_shape.num_elements().unwrap_or(0) as u32,
879        indices_len: indices_shape.num_elements().unwrap_or(0) as u32,
880        out_len: node.shape.num_elements().unwrap_or(0) as u32,
881        indices_i64: u8::from(indices_shape.dtype() == rlx_ir::DType::I64),
882        data_elem_bytes: data_shape.dtype().size_bytes() as u8,
883        axis: *axis,
884    }
885}
886
887#[allow(unused_variables)]
888pub(crate) fn compile_compare(
889    node: &rlx_ir::Node,
890    graph: &Graph,
891    arena: &crate::arena::Arena,
892    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
893    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
894    rng: rlx_ir::RngOptions,
895) -> Thunk {
896    let Op::Compare(cmp) = &node.op else {
897        unreachable!()
898    };
899    {
900        let len = node.shape.num_elements().unwrap();
901        let lhs_n = graph.node(node.inputs[0]).shape.num_elements().unwrap();
902        let rhs_n = graph.node(node.inputs[1]).shape.num_elements().unwrap();
903        let lhs_scalar = lhs_n == 1 && len > 1;
904        let rhs_scalar = rhs_n == 1 && len > 1;
905        let in_dtype = graph.node(node.inputs[0]).shape.dtype();
906        let inputs_i64 = u8::from(in_dtype == rlx_ir::DType::I64);
907        // A *partial* broadcast operand (numel neither 1 nor `len`) — e.g. rhs
908        // `[.,L,1]` against `[.,L,L]` — cannot be indexed by the flat fast path,
909        // which would read `len` elements from a shorter operand (past its end,
910        // into adjacent arena memory → wrong mask). Emit shape-aware broadcast
911        // strides for BOTH operands so exec indexes via row-major coords, just
912        // like `BinaryFull`. Needs static dims; a dynamic shape falls back to
913        // the fast path (behavior unchanged there).
914        let lhs_partial = lhs_n != len && lhs_n != 1;
915        let rhs_partial = rhs_n != len && rhs_n != 1;
916        let out_dims_s = get_static_dims(graph, node.id);
917        let lhs_dims_s = get_static_dims(graph, node.inputs[0]);
918        let rhs_dims_s = get_static_dims(graph, node.inputs[1]);
919        let (out_dims_bcast, bcast_lhs_strides, bcast_rhs_strides) = if (lhs_partial || rhs_partial)
920            && !out_dims_s.is_empty()
921            && !lhs_dims_s.is_empty()
922            && !rhs_dims_s.is_empty()
923        {
924            (
925                out_dims_s.iter().map(|&d| d as u32).collect::<Vec<u32>>(),
926                broadcast_strides(&lhs_dims_s, &out_dims_s),
927                broadcast_strides(&rhs_dims_s, &out_dims_s),
928            )
929        } else {
930            (Vec::new(), Vec::new(), Vec::new())
931        };
932        Thunk::Compare {
933            lhs: node_offset(arena, node.inputs[0]),
934            rhs: node_offset(arena, node.inputs[1]),
935            dst: node_offset(arena, node.id),
936            len: len as u32,
937            op: *cmp,
938            inputs_i64,
939            inputs_elem_bytes: in_dtype.size_bytes() as u8,
940            dst_elem_bytes: node.shape.dtype().size_bytes() as u8,
941            lhs_scalar,
942            rhs_scalar,
943            lhs_len: lhs_n as u32,
944            rhs_len: rhs_n as u32,
945            out_dims_bcast,
946            bcast_lhs_strides,
947            bcast_rhs_strides,
948        }
949    }
950}
951
952#[allow(unused_variables)]
953pub(crate) fn compile_where(
954    node: &rlx_ir::Node,
955    graph: &Graph,
956    arena: &crate::arena::Arena,
957    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
958    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
959    rng: rlx_ir::RngOptions,
960) -> Thunk {
961    let Op::Where = &node.op else { unreachable!() };
962    {
963        let len = node.shape.num_elements().unwrap();
964        let cn = graph.node(node.inputs[0]).shape.num_elements().unwrap();
965        let tn = graph.node(node.inputs[1]).shape.num_elements().unwrap();
966        let fn_ = graph.node(node.inputs[2]).shape.num_elements().unwrap();
967        let cond_scalar = cn == 1 && len > 1;
968        let true_scalar = tn == 1 && len > 1;
969        let false_scalar = fn_ == 1 && len > 1;
970        let elem_bytes = node.shape.dtype().size_bytes() as u8;
971        let cond_elem_bytes = graph.node(node.inputs[0]).shape.dtype().size_bytes() as u8;
972        Thunk::Where {
973            cond: node_offset(arena, node.inputs[0]),
974            on_true: node_offset(arena, node.inputs[1]),
975            on_false: node_offset(arena, node.inputs[2]),
976            dst: node_offset(arena, node.id),
977            len: len as u32,
978            elem_bytes,
979            cond_elem_bytes,
980            cond_scalar,
981            true_scalar,
982            false_scalar,
983        }
984    }
985}
986
987#[allow(unused_variables)]
988pub(crate) fn compile_fma(
989    node: &rlx_ir::Node,
990    graph: &Graph,
991    arena: &crate::arena::Arena,
992    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
993    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
994    rng: rlx_ir::RngOptions,
995) -> Thunk {
996    let Op::Fma = &node.op else { unreachable!() };
997    {
998        let len = node.shape.num_elements().unwrap();
999        Thunk::Fma {
1000            a: node_offset(arena, node.inputs[0]),
1001            b: node_offset(arena, node.inputs[1]),
1002            c: node_offset(arena, node.inputs[2]),
1003            dst: node_offset(arena, node.id),
1004            len: len as u32,
1005            elem_bytes: node.shape.dtype().size_bytes() as u8,
1006        }
1007    }
1008}
1009
1010#[allow(unused_variables)]
1011pub(crate) fn compile_relu_backward(
1012    node: &rlx_ir::Node,
1013    graph: &Graph,
1014    arena: &crate::arena::Arena,
1015    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
1016    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
1017    rng: rlx_ir::RngOptions,
1018) -> Thunk {
1019    let Op::ReluBackward = &node.op else {
1020        unreachable!()
1021    };
1022    {
1023        let len: usize = (0..node.shape.rank())
1024            .map(|i| node.shape.dim(i).unwrap_static())
1025            .product();
1026        let x = node_offset(arena, node.inputs[0]);
1027        let dy = node_offset(arena, node.inputs[1]);
1028        let dx = node_offset(arena, node.id);
1029        match node.shape.dtype() {
1030            rlx_ir::DType::F64 => Thunk::ReluBackwardF64 {
1031                x,
1032                dy,
1033                dx,
1034                len: len as u32,
1035            },
1036            _ => Thunk::ReluBackward {
1037                x,
1038                dy,
1039                dx,
1040                len: len as u32,
1041            },
1042        }
1043    }
1044}
1045
1046#[allow(unused_variables)]
1047pub(crate) fn compile_activation_backward(
1048    node: &rlx_ir::Node,
1049    graph: &Graph,
1050    arena: &crate::arena::Arena,
1051    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
1052    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
1053    rng: rlx_ir::RngOptions,
1054) -> Thunk {
1055    let Op::ActivationBackward { kind } = &node.op else {
1056        unreachable!()
1057    };
1058    {
1059        let len: usize = (0..node.shape.rank())
1060            .map(|i| node.shape.dim(i).unwrap_static())
1061            .product();
1062        let x = node_offset(arena, node.inputs[0]);
1063        let dy = node_offset(arena, node.inputs[1]);
1064        let dx = node_offset(arena, node.id);
1065        match node.shape.dtype() {
1066            rlx_ir::DType::F64 => Thunk::ActivationBackwardF64 {
1067                x,
1068                dy,
1069                dx,
1070                len: len as u32,
1071                kind: *kind,
1072            },
1073            _ => Thunk::ActivationBackward {
1074                x,
1075                dy,
1076                dx,
1077                len: len as u32,
1078                kind: *kind,
1079            },
1080        }
1081    }
1082}
1083
1084#[allow(unused_variables)]
1085pub(crate) fn compile_gather_backward(
1086    node: &rlx_ir::Node,
1087    graph: &Graph,
1088    arena: &crate::arena::Arena,
1089    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
1090    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
1091    rng: rlx_ir::RngOptions,
1092) -> Thunk {
1093    let Op::GatherBackward { .. } = &node.op else {
1094        unreachable!()
1095    };
1096    {
1097        let dy_shape = &graph.node(node.inputs[0]).shape;
1098        let idx_shape = &graph.node(node.inputs[1]).shape;
1099        let out_shape = &node.shape;
1100        let rank = out_shape.rank();
1101        let axis = match &node.op {
1102            Op::GatherBackward { axis } => *axis,
1103            _ => 0,
1104        };
1105        let axis_u = if axis < 0 {
1106            (rank as i32 + axis) as usize
1107        } else {
1108            axis as usize
1109        };
1110        let outer: usize = (0..axis_u)
1111            .map(|i| dy_shape.dim(i).unwrap_static())
1112            .product::<usize>()
1113            .max(1);
1114        let num_idx = idx_shape.dim(axis_u).unwrap_static();
1115        let trailing: usize = (axis_u + 1..dy_shape.rank())
1116            .map(|i| dy_shape.dim(i).unwrap_static())
1117            .product::<usize>()
1118            .max(1);
1119        let axis_dim = out_shape.dim(axis_u).unwrap_static();
1120        Thunk::GatherBackward {
1121            dy: node_offset(arena, node.inputs[0]),
1122            indices: node_offset(arena, node.inputs[1]),
1123            dst: node_offset(arena, node.id),
1124            outer: outer as u32,
1125            axis_dim: axis_dim as u32,
1126            num_idx: num_idx as u32,
1127            trailing: trailing as u32,
1128        }
1129    }
1130}
1131
1132#[allow(unused_variables)]
1133pub(crate) fn compile_concat(
1134    node: &rlx_ir::Node,
1135    graph: &Graph,
1136    arena: &crate::arena::Arena,
1137    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
1138    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
1139    rng: rlx_ir::RngOptions,
1140) -> Thunk {
1141    let Op::Concat { axis } = &node.op else {
1142        unreachable!()
1143    };
1144    {
1145        // Compute outer/inner from the OUTPUT shape: all inputs share
1146        // the same shape except along `axis`. The output's leading
1147        // and trailing dims match.
1148        let out_shape = &node.shape;
1149        let rank = out_shape.rank();
1150        let outer: usize = (0..*axis)
1151            .map(|i| out_shape.dim(i).unwrap_static())
1152            .product::<usize>()
1153            .max(1);
1154        let inner: usize = (*axis + 1..rank)
1155            .map(|i| out_shape.dim(i).unwrap_static())
1156            .product::<usize>()
1157            .max(1);
1158        let total_axis = out_shape.dim(*axis).unwrap_static();
1159        let inputs: Vec<(usize, u32, u32)> = node
1160            .inputs
1161            .iter()
1162            .map(|&in_id| {
1163                let in_shape = &graph.node(in_id).shape;
1164                let in_axis = concat_axis_extent(in_shape, *axis, rank);
1165                let in_numel = in_shape.num_elements().unwrap_or(0) as u32;
1166                (node_offset(arena, in_id), in_axis as u32, in_numel)
1167            })
1168            .collect();
1169        let dst = node_offset(arena, node.id);
1170        // The `Concat` thunk copies 4-byte (f32-sized) elements; `ConcatF64`
1171        // copies 8-byte elements. Route by element SIZE, not just F64 — an I64
1172        // tensor (8-byte) sent through the 4-byte path reads/writes half the
1173        // bytes per element, so it copies only the first half of each input and
1174        // leaves the tail zero (e.g. `Tile([9..0], 3)` on i64 rel-shift indices
1175        // → `[9,8,7,6,5]×3 + zeros`). The 8-byte copy is a pure byte move, so
1176        // reinterpreting i64 as f64 for the copy is bit-preserving.
1177        match out_shape.dtype().size_bytes() {
1178            8 => Thunk::ConcatF64 {
1179                dst,
1180                outer: outer as u32,
1181                inner: inner as u32,
1182                total_axis: total_axis as u32,
1183                inputs,
1184            },
1185            _ => Thunk::Concat {
1186                dst,
1187                outer: outer as u32,
1188                inner: inner as u32,
1189                total_axis: total_axis as u32,
1190                inputs,
1191            },
1192        }
1193    }
1194}
1195
1196#[allow(unused_variables)]
1197pub(crate) fn compile_elementwise_region(
1198    node: &rlx_ir::Node,
1199    graph: &Graph,
1200    arena: &crate::arena::Arena,
1201    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
1202    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
1203    rng: rlx_ir::RngOptions,
1204) -> Thunk {
1205    let Op::ElementwiseRegion {
1206        chain,
1207        scalar_input_mask,
1208        input_modulus,
1209        prologue,
1210        ..
1211    } = &node.op
1212    else {
1213        unreachable!()
1214    };
1215    {
1216        // The scalar interpreter handles plain chains; prologue (resize)
1217        // regions are GPU-only and never reach here on the CPU path
1218        // (the graphfused fusion options disable prologue/FK fusion).
1219        if *prologue != rlx_ir::op::RegionPrologue::None {
1220            Thunk::Nop
1221        } else {
1222            let input_offs: Vec<usize> = node
1223                .inputs
1224                .iter()
1225                .map(|&id| node_offset(arena, id))
1226                .collect();
1227            Thunk::ElementwiseRegion {
1228                dst: node_offset(arena, node.id),
1229                len: node.shape.num_elements().unwrap_or(0) as u32,
1230                input_offs,
1231                chain: chain.clone(),
1232                scalar_input_mask: *scalar_input_mask,
1233                input_modulus: *input_modulus,
1234            }
1235        }
1236    }
1237}
1238
1239pub(crate) fn get_len(graph: &Graph, id: NodeId) -> usize {
1240    graph.node(id).shape.num_elements().unwrap_or(0)
1241}
1242
1243/// Static `usize` dims of a node's shape, or empty if any dim is dynamic.
1244pub(crate) fn get_static_dims(graph: &Graph, id: NodeId) -> Vec<usize> {
1245    let dims = graph.node(id).shape.dims();
1246    let mut out = Vec::with_capacity(dims.len());
1247    for d in dims {
1248        if let Some(s) = match d {
1249            rlx_ir::Dim::Static(s) => Some(*s),
1250            _ => None,
1251        } {
1252            out.push(s);
1253        } else {
1254            return Vec::new();
1255        }
1256    }
1257    out
1258}
1259
1260/// Extent along `axis` for a concat input, treating leading implicit 1s when
1261/// `input.rank() < output.rank()` (ONNX / numpy concat broadcast rules).
1262pub(crate) fn concat_axis_extent(input: &rlx_ir::Shape, axis: usize, out_rank: usize) -> usize {
1263    let in_rank = input.rank();
1264    if axis >= out_rank {
1265        return 1;
1266    }
1267    if axis < in_rank {
1268        input.dim(axis).unwrap_static()
1269    } else {
1270        1
1271    }
1272}
1273
1274pub(crate) fn broadcast_src_index(src_idx: usize, in_len: usize) -> usize {
1275    if in_len == 0 { 0 } else { src_idx % in_len }
1276}
1277
1278/// Copy `inp` into row-strided `out` for a Concat input, with outer-dim
1279/// broadcasting. The body is dtype-agnostic (only `Copy` + slice ops), so the
1280/// f32/f64 variants are generated from one template rather than copy-pasted.
1281macro_rules! concat_copy_rows {
1282    ($name:ident, $t:ty) => {
1283        pub(crate) fn $name(
1284            out: &mut [$t],
1285            inp: &[$t],
1286            outer: usize,
1287            copy_per_row: usize,
1288            row_stride: usize,
1289            dst_col_off: usize,
1290            in_numel: usize,
1291        ) {
1292            let need = outer.saturating_mul(copy_per_row.max(1));
1293            let broadcast_outer = in_numel < need;
1294            let out_ptr = out.as_mut_ptr() as usize;
1295            let inp_ptr = inp.as_ptr() as usize;
1296            let inp_len = inp.len();
1297            let run = |o0: usize, o1: usize| {
1298                let out = unsafe { std::slice::from_raw_parts_mut(out_ptr as *mut $t, out.len()) };
1299                let inp = unsafe { std::slice::from_raw_parts(inp_ptr as *const $t, inp_len) };
1300                for o in o0..o1 {
1301                    let dst_row_start = o * row_stride + dst_col_off;
1302                    if broadcast_outer {
1303                        if in_numel == 1 {
1304                            if copy_per_row == 1 {
1305                                out[dst_row_start] = inp[0];
1306                            } else {
1307                                out[dst_row_start..dst_row_start + copy_per_row].fill(inp[0]);
1308                            }
1309                        } else if copy_per_row <= inp.len() {
1310                            out[dst_row_start..dst_row_start + copy_per_row]
1311                                .copy_from_slice(&inp[..copy_per_row]);
1312                        } else if !inp.is_empty() {
1313                            out[dst_row_start..dst_row_start + copy_per_row].fill(inp[0]);
1314                        }
1315                    } else {
1316                        let src_row_start = o * copy_per_row;
1317                        out[dst_row_start..dst_row_start + copy_per_row]
1318                            .copy_from_slice(&inp[src_row_start..src_row_start + copy_per_row]);
1319                    }
1320                }
1321            };
1322            // F5 DiT concatenates large mel/text streams — parallelize outer rows.
1323            if outer >= 8
1324                && copy_per_row >= 16
1325                && crate::pool::num_threads() > 1
1326                && crate::pool::should_parallelize(outer.saturating_mul(copy_per_row))
1327            {
1328                crate::pool::par_for(outer, 1, &|off, cnt| run(off, off + cnt));
1329            } else {
1330                run(0, outer);
1331            }
1332        }
1333    };
1334}
1335
1336concat_copy_rows!(concat_copy_rows_f32, f32);
1337concat_copy_rows!(concat_copy_rows_f64, f64);
1338
1339/// NumPy-style broadcast strides for one operand into the flat output
1340/// buffer. Returns a length-`out_dims.len()` `Vec<u32>` where entry
1341/// `d` is `0` if the input is size-1 (broadcast) at output dim `d`
1342/// (after left-padding with size-1 to match ranks), otherwise the
1343/// natural row-major stride into the *input* buffer.
1344///
1345/// Caller iterates output flat index `i` → output coords (row-major)
1346/// → input flat index = dot(coords, strides). The result is correct
1347/// for any broadcast pattern (scalar, last-axis, middle-axis,
1348/// bidirectional).
1349/// True when `rhs_dims` describes a *trailing* broadcast of `out_dims`
1350/// — i.e. every rhs dim either equals the corresponding output dim
1351/// (counting from the right) or rhs is shorter (left-padded with 1s).
1352/// Mid-shape singletons (e.g. rhs `[a, b, 1, d]` into out `[a, b, c, d]`
1353/// where `c > 1`) are NOT trailing broadcasts and require the
1354/// shape-aware `BinaryFull` slow path — `BiasAdd`'s linear bias-replicated
1355/// kernel silently miscomputes them.
1356pub(crate) fn is_trailing_bias_broadcast(
1357    rhs_dims: &[rlx_ir::Dim],
1358    out_dims: &[rlx_ir::Dim],
1359) -> bool {
1360    if rhs_dims.len() > out_dims.len() {
1361        return false;
1362    }
1363    let off = out_dims.len() - rhs_dims.len();
1364    for i in 0..rhs_dims.len() {
1365        let r = match rhs_dims[i] {
1366            rlx_ir::Dim::Static(n) => n,
1367            _ => return false,
1368        };
1369        let o = match out_dims[off + i] {
1370            rlx_ir::Dim::Static(n) => n,
1371            _ => return false,
1372        };
1373        if r != o {
1374            return false;
1375        }
1376    }
1377    true
1378}
1379
1380pub(crate) fn broadcast_strides(in_dims: &[usize], out_dims: &[usize]) -> Vec<u32> {
1381    let r_out = out_dims.len();
1382    let r_in = in_dims.len();
1383    assert!(
1384        r_in <= r_out,
1385        "broadcast: input rank {r_in} > output rank {r_out}"
1386    );
1387    let pad = r_out - r_in;
1388    let mut strides = vec![0u32; r_out];
1389    let mut acc: usize = 1;
1390    for d in (0..r_out).rev() {
1391        let in_size = if d < pad { 1 } else { in_dims[d - pad] };
1392        if in_size == 1 {
1393            strides[d] = 0;
1394        } else {
1395            assert_eq!(
1396                in_size, out_dims[d],
1397                "broadcast: input dim {in_size} doesn't match output dim {} at axis {d}",
1398                out_dims[d]
1399            );
1400            strides[d] = acc as u32;
1401            acc *= in_size;
1402        }
1403    }
1404    strides
1405}
1406
1407#[inline(always)]
1408pub(crate) fn exec_elementwise_region(t: &Thunk, base: *mut u8) {
1409    let Thunk::ElementwiseRegion {
1410        dst,
1411        len,
1412        input_offs,
1413        chain,
1414        scalar_input_mask,
1415        input_modulus,
1416    } = t
1417    else {
1418        unreachable!()
1419    };
1420    {
1421        let len = *len as usize;
1422        if !chain.is_empty() && len > 0 {
1423            let base_addr = base as usize;
1424            let dst = *dst;
1425            let scalar_mask = *scalar_input_mask;
1426            // Each output element is independent → fan over the range.
1427            let eval = |gid: usize| {
1428                let v = region_eval_elem(
1429                    gid,
1430                    base_addr as *const u8,
1431                    input_offs,
1432                    chain,
1433                    scalar_mask,
1434                    input_modulus,
1435                );
1436                unsafe {
1437                    *((base_addr as *mut u8).add(dst) as *mut f32).add(gid) = v;
1438                }
1439            };
1440            if fast_conv_enabled() && crate::pool::should_parallelize(len) {
1441                crate::pool::par_for(len, crate::pool::chunk_floor(len), &|off, cnt| {
1442                    for gid in off..off + cnt {
1443                        eval(gid);
1444                    }
1445                });
1446            } else {
1447                for gid in 0..len {
1448                    eval(gid);
1449                }
1450            }
1451        }
1452    }
1453}
1454
1455#[inline(always)]
1456pub(crate) fn exec_transpose_f64(t: &Thunk, base: *mut u8) {
1457    let Thunk::TransposeF64 {
1458        src,
1459        dst,
1460        in_total,
1461        out_dims,
1462        in_strides,
1463    } = t
1464    else {
1465        unreachable!()
1466    };
1467    unsafe {
1468        let inp = sl_f64(*src, base, *in_total as usize);
1469        let out_total: usize = out_dims.iter().map(|d| *d as usize).product();
1470        let out = sl_mut_f64(*dst, base, out_total);
1471        transpose_walk_f64(inp, out, out_dims, in_strides);
1472    }
1473}
1474
1475#[inline(always)]
1476pub(crate) fn exec_activation_f64(t: &Thunk, base: *mut u8) {
1477    let Thunk::ActivationF64 {
1478        src,
1479        dst,
1480        len,
1481        kind,
1482    } = t
1483    else {
1484        unreachable!()
1485    };
1486    {
1487        let len = *len as usize;
1488        unsafe {
1489            let inp = sl_f64(*src, base, len);
1490            let out = sl_mut_f64(*dst, base, len);
1491            apply_activation_f64(inp, out, *kind);
1492        }
1493    }
1494}
1495
1496#[inline(always)]
1497pub(crate) fn exec_binary_full_f64(t: &Thunk, base: *mut u8) {
1498    let Thunk::BinaryFullF64 {
1499        lhs,
1500        rhs,
1501        dst,
1502        len,
1503        lhs_len,
1504        rhs_len,
1505        op,
1506        out_dims_bcast,
1507        bcast_lhs_strides,
1508        bcast_rhs_strides,
1509    } = t
1510    else {
1511        unreachable!()
1512    };
1513    {
1514        let len = *len as usize;
1515        let lhs_len = *lhs_len as usize;
1516        let rhs_len = *rhs_len as usize;
1517        unsafe {
1518            let l = sl_f64(*lhs, base, lhs_len);
1519            let r = sl_f64(*rhs, base, rhs_len);
1520            let d = sl_mut_f64(*dst, base, len);
1521            if lhs_len == len && rhs_len == len {
1522                for i in 0..len {
1523                    d[i] = binary_op_f64(*op, l[i], r[i]);
1524                }
1525            } else if !out_dims_bcast.is_empty() {
1526                // Shape-aware broadcast path: correct for
1527                // arbitrary NumPy-style broadcasts including
1528                // bidirectional `[N,1] op [1,S]`.
1529                let rank = out_dims_bcast.len();
1530                let mut coords = vec![0u32; rank];
1531                for i in 0..len {
1532                    let mut rem = i;
1533                    for ax in (0..rank).rev() {
1534                        let sz = out_dims_bcast[ax] as usize;
1535                        coords[ax] = (rem % sz) as u32;
1536                        rem /= sz;
1537                    }
1538                    let mut li: usize = 0;
1539                    let mut ri: usize = 0;
1540                    for ax in 0..rank {
1541                        li += coords[ax] as usize * bcast_lhs_strides[ax] as usize;
1542                        ri += coords[ax] as usize * bcast_rhs_strides[ax] as usize;
1543                    }
1544                    d[i] = binary_op_f64(*op, l[li], r[ri]);
1545                }
1546            } else {
1547                // Fallback: legacy modulo path (preserved for
1548                // dynamic-shape graphs where strides can't be
1549                // precomputed). Only correct for scalar /
1550                // last-axis broadcast.
1551                for i in 0..len {
1552                    d[i] = binary_op_f64(*op, l[i % lhs_len], r[i % rhs_len]);
1553                }
1554            }
1555        }
1556    }
1557}
1558
1559#[inline(always)]
1560pub(crate) fn exec_binary_full_c64(t: &Thunk, base: *mut u8) {
1561    let Thunk::BinaryFullC64 {
1562        lhs,
1563        rhs,
1564        dst,
1565        len,
1566        lhs_len,
1567        rhs_len,
1568        op,
1569        out_dims_bcast,
1570        bcast_lhs_strides,
1571        bcast_rhs_strides,
1572    } = t
1573    else {
1574        unreachable!()
1575    };
1576    {
1577        // Complex element layout: [re_0, im_0, re_1, im_1, ...]
1578        // Underlying f32 buffer length is 2·N (N = complex
1579        // element count). All offsets are byte offsets; the
1580        // `sl` helper reads as f32 starting at the byte
1581        // offset, so f32-length = 2·complex-len.
1582        let n_out = *len as usize;
1583        let n_l = *lhs_len as usize;
1584        let n_r = *rhs_len as usize;
1585        unsafe {
1586            let l = sl(*lhs, base, 2 * n_l);
1587            let r = sl(*rhs, base, 2 * n_r);
1588            let d = sl_mut(*dst, base, 2 * n_out);
1589            let do_c64 = |a_re: f32, a_im: f32, b_re: f32, b_im: f32| -> (f32, f32) {
1590                match op {
1591                    BinaryOp::Add => (a_re + b_re, a_im + b_im),
1592                    BinaryOp::Sub => (a_re - b_re, a_im - b_im),
1593                    BinaryOp::Mul => (a_re * b_re - a_im * b_im, a_re * b_im + a_im * b_re),
1594                    BinaryOp::Div => {
1595                        let denom = b_re * b_re + b_im * b_im;
1596                        (
1597                            (a_re * b_re + a_im * b_im) / denom,
1598                            (a_im * b_re - a_re * b_im) / denom,
1599                        )
1600                    }
1601                    BinaryOp::Max
1602                    | BinaryOp::Min
1603                    | BinaryOp::Pow
1604                    | BinaryOp::Mod
1605                    | BinaryOp::BitAnd
1606                    | BinaryOp::BitOr
1607                    | BinaryOp::BitXor
1608                    | BinaryOp::Shl
1609                    | BinaryOp::Shr
1610                    | BinaryOp::Atan2 => {
1611                        unreachable!("C64 max/min/pow/mod/bitwise/atan2 rejected at lowering")
1612                    }
1613                }
1614            };
1615            if n_l == n_out && n_r == n_out {
1616                for i in 0..n_out {
1617                    let (re, im) = do_c64(l[2 * i], l[2 * i + 1], r[2 * i], r[2 * i + 1]);
1618                    d[2 * i] = re;
1619                    d[2 * i + 1] = im;
1620                }
1621            } else if !out_dims_bcast.is_empty() {
1622                // Strided complex broadcast: strides are in
1623                // *complex element* units; multiply by 2 when
1624                // indexing into the f32 buffer.
1625                let rank = out_dims_bcast.len();
1626                let mut coords = vec![0u32; rank];
1627                for i in 0..n_out {
1628                    let mut rem = i;
1629                    for ax in (0..rank).rev() {
1630                        let sz = out_dims_bcast[ax] as usize;
1631                        coords[ax] = (rem % sz) as u32;
1632                        rem /= sz;
1633                    }
1634                    let mut li: usize = 0;
1635                    let mut ri: usize = 0;
1636                    for ax in 0..rank {
1637                        li += coords[ax] as usize * bcast_lhs_strides[ax] as usize;
1638                        ri += coords[ax] as usize * bcast_rhs_strides[ax] as usize;
1639                    }
1640                    let (re, im) = do_c64(l[2 * li], l[2 * li + 1], r[2 * ri], r[2 * ri + 1]);
1641                    d[2 * i] = re;
1642                    d[2 * i + 1] = im;
1643                }
1644            } else {
1645                // Modulo fallback (scalar / last-axis broadcast).
1646                for i in 0..n_out {
1647                    let li = if n_l == 1 { 0 } else { i % n_l };
1648                    let ri = if n_r == 1 { 0 } else { i % n_r };
1649                    let (re, im) = do_c64(l[2 * li], l[2 * li + 1], r[2 * ri], r[2 * ri + 1]);
1650                    d[2 * i] = re;
1651                    d[2 * i + 1] = im;
1652                }
1653            }
1654        }
1655    }
1656}
1657
1658#[inline(always)]
1659pub(crate) fn exec_activation_c64(t: &Thunk, base: *mut u8) {
1660    let Thunk::ActivationC64 {
1661        src,
1662        dst,
1663        len,
1664        kind,
1665    } = t
1666    else {
1667        unreachable!()
1668    };
1669    {
1670        let n = *len as usize;
1671        unsafe {
1672            let s = sl(*src, base, 2 * n);
1673            let d = sl_mut(*dst, base, 2 * n);
1674            for i in 0..n {
1675                let a = s[2 * i];
1676                let b = s[2 * i + 1];
1677                let (re, im) = match kind {
1678                    Activation::Neg => (-a, -b),
1679                    Activation::Exp => {
1680                        // exp(a + bi) = e^a · (cos b + i·sin b)
1681                        let ea = a.exp();
1682                        (ea * b.cos(), ea * b.sin())
1683                    }
1684                    Activation::Log => {
1685                        // log(z) = log|z| + i·arg(z), principal branch
1686                        let r = (a * a + b * b).sqrt();
1687                        (r.ln(), b.atan2(a))
1688                    }
1689                    Activation::Sqrt => {
1690                        // sqrt(a+bi) = sqrt((|z|+a)/2) + sign(b)·i·sqrt((|z|-a)/2)
1691                        // Principal branch; for b == 0 and a < 0 returns +i·sqrt(|a|).
1692                        let r = (a * a + b * b).sqrt();
1693                        let re = ((r + a) * 0.5).max(0.0).sqrt();
1694                        let im_mag = ((r - a) * 0.5).max(0.0).sqrt();
1695                        let im = if b >= 0.0 { im_mag } else { -im_mag };
1696                        (re, im)
1697                    }
1698                    _ => unreachable!("non-C64 activation kind survived lowering"),
1699                };
1700                d[2 * i] = re;
1701                d[2 * i + 1] = im;
1702            }
1703        }
1704    }
1705}
1706
1707#[inline(always)]
1708pub(crate) fn exec_gather(t: &Thunk, base: *mut u8) {
1709    let Thunk::Gather {
1710        table,
1711        table_len,
1712        idx,
1713        dst,
1714        num_idx,
1715        trailing,
1716        idx_i64,
1717        table_bytes,
1718    } = t
1719    else {
1720        unreachable!()
1721    };
1722    {
1723        let (ni, tr) = (*num_idx as usize, *trailing as usize);
1724        let rows = *table_len as usize / tr.max(1);
1725        unsafe {
1726            if *table_bytes == 8 {
1727                let tab = sl_i64(*table, base, *table_len as usize);
1728                let out = sl_mut_i64(*dst, base, ni * tr);
1729                if *idx_i64 != 0 {
1730                    let ids = sl_i64(*idx, base, ni);
1731                    for i in 0..ni {
1732                        let row = ids[i].max(0) as usize;
1733                        if row < rows {
1734                            out[i * tr..(i + 1) * tr]
1735                                .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
1736                        }
1737                    }
1738                } else {
1739                    let ids = sl(*idx, base, ni);
1740                    for i in 0..ni {
1741                        let row = ids[i] as usize;
1742                        if row < rows {
1743                            out[i * tr..(i + 1) * tr]
1744                                .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
1745                        }
1746                    }
1747                }
1748            } else {
1749                let tab = sl(*table, base, *table_len as usize);
1750                let out = sl_mut(*dst, base, ni * tr);
1751                if *idx_i64 != 0 {
1752                    let ids = sl_i64(*idx, base, ni);
1753                    for i in 0..ni {
1754                        let row = ids[i].max(0) as usize;
1755                        if row < rows {
1756                            out[i * tr..(i + 1) * tr]
1757                                .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
1758                        }
1759                    }
1760                } else {
1761                    let ids = sl(*idx, base, ni);
1762                    for i in 0..ni {
1763                        let row = ids[i] as usize;
1764                        if row < rows {
1765                            out[i * tr..(i + 1) * tr]
1766                                .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
1767                        }
1768                    }
1769                }
1770            }
1771        }
1772    }
1773}
1774
1775#[inline(always)]
1776pub(crate) fn exec_activation_in_place(t: &Thunk, base: *mut u8) {
1777    let Thunk::ActivationInPlace { data, len, act } = t else {
1778        unreachable!()
1779    };
1780    {
1781        let len = *len as usize;
1782        unsafe {
1783            let d = sl_mut(*data, base, len);
1784            // Serial scalar transcendentals were a per-op cost for models heavy in
1785            // tanh/sigmoid/exp (LSTM/GRU gates, vocoders). Apply elementwise in
1786            // parallel for large tensors (bit-exact — elements independent), mirroring
1787            // the already-parallel Gelu/Silu kernels. `should_parallelize` guards the
1788            // small case so tiny activations stay serial (no dispatch overhead).
1789            let par = crate::pool::should_parallelize(len);
1790            macro_rules! apply {
1791                ($f:expr) => {{
1792                    let f = $f;
1793                    if par {
1794                        use rayon::prelude::*;
1795                        d.par_iter_mut().for_each(|v| *v = f(*v));
1796                    } else {
1797                        for v in d.iter_mut() {
1798                            *v = f(*v);
1799                        }
1800                    }
1801                }};
1802            }
1803            match act {
1804                Activation::Gelu => crate::kernels::par_gelu_inplace(d),
1805                Activation::GeluApprox => crate::kernels::par_gelu_approx_inplace(d),
1806                Activation::Silu => crate::kernels::par_silu_inplace(d),
1807                Activation::Relu => apply!(|x: f32| x.max(0.0)),
1808                Activation::Sigmoid => apply!(|x: f32| 1.0 / (1.0 + (-x).exp())),
1809                Activation::Tanh => crate::vmath::vvtanhf_hot_inplace(d),
1810                Activation::Exp => crate::vmath::vvexpf_hot_inplace(d),
1811                Activation::Log => crate::vmath::vvlogf_inplace(d),
1812                Activation::Sqrt => crate::vmath::vvsqrtf_inplace(d),
1813                Activation::Rsqrt => crate::vmath::vvrsqrtf_inplace(d),
1814                Activation::Neg => apply!(|x: f32| -x),
1815                Activation::Abs => apply!(|x: f32| x.abs()),
1816                Activation::Round => apply!(|x: f32| x.round()),
1817                Activation::Sin => apply!(|x: f32| x.sin()),
1818                Activation::Cos => apply!(|x: f32| x.cos()),
1819                Activation::Tan => apply!(|x: f32| x.tan()),
1820                Activation::Atan => apply!(|x: f32| x.atan()),
1821                Activation::Recip => crate::vmath::vvrecf_inplace(d),
1822                Activation::Floor => apply!(|x: f32| x.floor()),
1823                Activation::Ceil => apply!(|x: f32| x.ceil()),
1824                Activation::Sign => {
1825                    apply!(|x: f32| if x > 0.0 {
1826                        1.0
1827                    } else if x < 0.0 {
1828                        -1.0
1829                    } else {
1830                        0.0
1831                    })
1832                }
1833                Activation::Softplus => apply!(|x: f32| x.max(0.0) + (-(x.abs())).exp().ln_1p()),
1834                Activation::Elu => apply!(|x: f32| if x > 0.0 { x } else { x.exp() - 1.0 }),
1835                Activation::Erf => apply!(|x: f32| erf_f32(x)),
1836                Activation::HardSwish => apply!(|x: f32| x * (x + 3.0).clamp(0.0, 6.0) / 6.0),
1837                Activation::HardSigmoid => apply!(|x: f32| (x / 6.0 + 0.5).clamp(0.0, 1.0)),
1838                Activation::Mish => {
1839                    apply!(|x: f32| x * (x.max(0.0) + (-(x.abs())).exp().ln_1p()).tanh())
1840                }
1841                Activation::Softsign => apply!(|x: f32| x / (1.0 + x.abs())),
1842                Activation::LogSigmoid => {
1843                    apply!(|x: f32| x.min(0.0) - (-(x.abs())).exp().ln_1p())
1844                }
1845            }
1846        }
1847    }
1848}
1849
1850#[inline(always)]
1851pub(crate) fn exec_concat(t: &Thunk, base: *mut u8) {
1852    let Thunk::Concat {
1853        dst,
1854        outer,
1855        inner,
1856        total_axis,
1857        inputs,
1858    } = t
1859    else {
1860        unreachable!()
1861    };
1862    {
1863        let outer = *outer as usize;
1864        let inner = *inner as usize;
1865        let total_axis = *total_axis as usize;
1866        let row_stride = total_axis * inner;
1867        let out_total = outer * row_stride;
1868        unsafe {
1869            let out = sl_mut(*dst, base, out_total);
1870            let mut cum: usize = 0;
1871            for (src_off, in_axis, in_numel) in inputs {
1872                let in_axis = *in_axis as usize;
1873                let copy_per_row = in_axis * inner;
1874                let dst_col_off = cum * inner;
1875                let inp = sl(*src_off, base, (*in_numel as usize).max(1));
1876                concat_copy_rows_f32(
1877                    out,
1878                    inp,
1879                    outer,
1880                    copy_per_row,
1881                    row_stride,
1882                    dst_col_off,
1883                    *in_numel as usize,
1884                );
1885                cum += in_axis;
1886            }
1887        }
1888    }
1889}
1890
1891#[inline(always)]
1892pub(crate) fn exec_concat_f64(t: &Thunk, base: *mut u8) {
1893    let Thunk::ConcatF64 {
1894        dst,
1895        outer,
1896        inner,
1897        total_axis,
1898        inputs,
1899    } = t
1900    else {
1901        unreachable!()
1902    };
1903    {
1904        let outer = *outer as usize;
1905        let inner = *inner as usize;
1906        let total_axis = *total_axis as usize;
1907        let row_stride = total_axis * inner;
1908        let out_total = outer * row_stride;
1909        unsafe {
1910            let out = sl_mut_f64(*dst, base, out_total);
1911            let mut cum: usize = 0;
1912            for (src_off, in_axis, in_numel) in inputs {
1913                let in_axis = *in_axis as usize;
1914                let copy_per_row = in_axis * inner;
1915                let dst_col_off = cum * inner;
1916                let inp = sl_f64(*src_off, base, (*in_numel as usize).max(1));
1917                concat_copy_rows_f64(
1918                    out,
1919                    inp,
1920                    outer,
1921                    copy_per_row,
1922                    row_stride,
1923                    dst_col_off,
1924                    *in_numel as usize,
1925                );
1926                cum += in_axis;
1927            }
1928        }
1929    }
1930}
1931
1932#[inline(always)]
1933pub(crate) fn exec_scatter_add(t: &Thunk, base: *mut u8) {
1934    let Thunk::ScatterAdd {
1935        updates,
1936        indices,
1937        dst,
1938        num_updates,
1939        out_dim,
1940        trailing,
1941    } = t
1942    else {
1943        unreachable!()
1944    };
1945    {
1946        let num_updates = *num_updates as usize;
1947        let out_dim = *out_dim as usize;
1948        let trailing = *trailing as usize;
1949        unsafe {
1950            let upd = sl(*updates, base, num_updates * trailing);
1951            let ids = sl(*indices, base, num_updates);
1952            let out = sl_mut(*dst, base, out_dim * trailing);
1953            // Zero the output first — semantics are accumulate-into-zeros.
1954            for v in out.iter_mut() {
1955                *v = 0.0;
1956            }
1957            for i in 0..num_updates {
1958                let row = ids[i] as usize;
1959                debug_assert!(row < out_dim, "ScatterAdd index out of range");
1960                let src_off = i * trailing;
1961                let dst_off = row * trailing;
1962                for j in 0..trailing {
1963                    out[dst_off + j] += upd[src_off + j];
1964                }
1965            }
1966        }
1967    }
1968}
1969
1970#[inline(always)]
1971pub(crate) fn exec_scatter_nd(t: &Thunk, base: *mut u8) {
1972    let Thunk::ScatterNd {
1973        data,
1974        indices,
1975        updates,
1976        dst,
1977        data_shape,
1978        indices_shape,
1979        data_len,
1980        updates_len,
1981        indices_len,
1982        indices_i64,
1983        reduction,
1984    } = t
1985    else {
1986        unreachable!()
1987    };
1988    let data_shape: Vec<usize> = data_shape.iter().map(|&d| d as usize).collect();
1989    let indices_shape: Vec<usize> = indices_shape.iter().map(|&d| d as usize).collect();
1990    let data_len = *data_len as usize;
1991    let updates_len = *updates_len as usize;
1992    let indices_len = *indices_len as usize;
1993    unsafe {
1994        let data_s = sl(*data, base, data_len);
1995        let updates_s = sl(*updates, base, updates_len);
1996        let out = sl_mut(*dst, base, data_len);
1997        let idx_i64: Vec<i64> = if *indices_i64 != 0 {
1998            let ptr = base.add(*indices) as *const i64;
1999            std::slice::from_raw_parts(ptr, indices_len).to_vec()
2000        } else {
2001            sl(*indices, base, indices_len)
2002                .iter()
2003                .map(|&x| x as i64)
2004                .collect()
2005        };
2006        let (offsets, slice) =
2007            crate::onnx_indexing::scatter_nd_dst_offsets(&data_shape, &idx_i64, &indices_shape);
2008        crate::onnx_indexing::scatter_nd_into_f32(
2009            data_s, updates_s, out, &offsets, slice, *reduction,
2010        );
2011    }
2012}
2013
2014fn load_indices_i64(base: *mut u8, off: usize, len: usize, as_i64: u8) -> Vec<i64> {
2015    unsafe {
2016        if as_i64 != 0 {
2017            let ptr = base.add(off) as *const i64;
2018            std::slice::from_raw_parts(ptr, len).to_vec()
2019        } else {
2020            sl(off, base, len).iter().map(|&x| x as i64).collect()
2021        }
2022    }
2023}
2024
2025#[inline(always)]
2026pub(crate) fn exec_scatter_elements(t: &Thunk, base: *mut u8) {
2027    let Thunk::ScatterElements {
2028        data,
2029        indices,
2030        updates,
2031        dst,
2032        data_shape,
2033        data_len,
2034        updates_len,
2035        indices_len,
2036        indices_i64,
2037        axis,
2038        reduction,
2039    } = t
2040    else {
2041        unreachable!()
2042    };
2043    let data_shape: Vec<usize> = data_shape.iter().map(|&d| d as usize).collect();
2044    unsafe {
2045        let data_s = sl(*data, base, *data_len as usize);
2046        let updates_s = sl(*updates, base, *updates_len as usize);
2047        let out = sl_mut(*dst, base, *data_len as usize);
2048        let idx = load_indices_i64(base, *indices, *indices_len as usize, *indices_i64);
2049        crate::onnx_indexing::scatter_elements_f32(
2050            data_s,
2051            updates_s,
2052            &idx,
2053            out,
2054            &data_shape,
2055            *axis,
2056            *reduction,
2057        );
2058    }
2059}
2060
2061#[inline(always)]
2062pub(crate) fn exec_gather_nd(t: &Thunk, base: *mut u8) {
2063    let Thunk::GatherNd {
2064        data,
2065        indices,
2066        dst,
2067        data_shape,
2068        indices_shape,
2069        data_len,
2070        indices_len,
2071        out_len,
2072        indices_i64,
2073        batch_dims,
2074    } = t
2075    else {
2076        unreachable!()
2077    };
2078    let data_shape: Vec<usize> = data_shape.iter().map(|&d| d as usize).collect();
2079    let indices_shape: Vec<usize> = indices_shape.iter().map(|&d| d as usize).collect();
2080    unsafe {
2081        let data_s = sl(*data, base, *data_len as usize);
2082        let out = sl_mut(*dst, base, *out_len as usize);
2083        let idx = load_indices_i64(base, *indices, *indices_len as usize, *indices_i64);
2084        let (offsets, slice) = crate::onnx_indexing::gather_nd_src_offsets(
2085            &data_shape,
2086            &idx,
2087            &indices_shape,
2088            (*batch_dims).max(0) as usize,
2089        );
2090        for (t, &off) in offsets.iter().enumerate() {
2091            let d0 = t * slice;
2092            for j in 0..slice {
2093                if let (Some(&s), Some(d)) = (data_s.get(off + j), out.get_mut(d0 + j)) {
2094                    *d = s;
2095                }
2096            }
2097        }
2098    }
2099}
2100
2101#[inline(always)]
2102pub(crate) fn exec_gather_elements(t: &Thunk, base: *mut u8) {
2103    let Thunk::GatherElements {
2104        data,
2105        indices,
2106        dst,
2107        data_shape,
2108        indices_shape,
2109        data_len,
2110        indices_len,
2111        out_len,
2112        indices_i64,
2113        data_elem_bytes,
2114        axis,
2115    } = t
2116    else {
2117        unreachable!()
2118    };
2119    let data_shape: Vec<usize> = data_shape.iter().map(|&d| d as usize).collect();
2120    let indices_shape: Vec<usize> = indices_shape.iter().map(|&d| d as usize).collect();
2121    let elem_bytes = (*data_elem_bytes as usize).max(1);
2122    unsafe {
2123        let idx = load_indices_i64(base, *indices, *indices_len as usize, *indices_i64);
2124        if elem_bytes == 4 {
2125            // Fast path preserves the (well-tested) f32 element copy.
2126            let data_s = sl(*data, base, *data_len as usize);
2127            let out = sl_mut(*dst, base, *out_len as usize);
2128            crate::onnx_indexing::gather_elements_f32(
2129                data_s,
2130                &idx,
2131                out,
2132                &data_shape,
2133                &indices_shape,
2134                *axis,
2135            );
2136        } else {
2137            // Dtype-agnostic byte copy for I64/I8/… data (e.g. int64 token ids).
2138            let data_b =
2139                std::slice::from_raw_parts(base.add(*data), *data_len as usize * elem_bytes);
2140            let out_b =
2141                std::slice::from_raw_parts_mut(base.add(*dst), *out_len as usize * elem_bytes);
2142            crate::onnx_indexing::gather_elements_bytes(
2143                data_b,
2144                &idx,
2145                out_b,
2146                &data_shape,
2147                &indices_shape,
2148                *axis,
2149                elem_bytes,
2150            );
2151        }
2152    }
2153}
2154
2155#[inline(always)]
2156pub(crate) fn exec_relu_backward(t: &Thunk, base: *mut u8) {
2157    let Thunk::ReluBackward { x, dy, dx, len } = t else {
2158        unreachable!()
2159    };
2160    {
2161        let len = *len as usize;
2162        unsafe {
2163            let xs = sl(*x, base, len);
2164            let dys = sl(*dy, base, len);
2165            let out = sl_mut(*dx, base, len);
2166            if fast_conv_enabled() && crate::pool::should_parallelize(len) {
2167                let oa = out.as_mut_ptr() as usize;
2168                crate::pool::par_for(len, crate::pool::chunk_floor(len), &|off, cnt| {
2169                    for i in off..off + cnt {
2170                        *((oa as *mut f32).add(i)) = if xs[i] > 0.0 { dys[i] } else { 0.0 };
2171                    }
2172                });
2173            } else {
2174                for i in 0..len {
2175                    out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
2176                }
2177            }
2178        }
2179    }
2180}
2181
2182#[inline(always)]
2183pub(crate) fn exec_relu_backward_f64(t: &Thunk, base: *mut u8) {
2184    let Thunk::ReluBackwardF64 { x, dy, dx, len } = t else {
2185        unreachable!()
2186    };
2187    {
2188        let len = *len as usize;
2189        unsafe {
2190            let xs = sl_f64(*x, base, len);
2191            let dys = sl_f64(*dy, base, len);
2192            let out = sl_mut_f64(*dx, base, len);
2193            for i in 0..len {
2194                out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
2195            }
2196        }
2197    }
2198}
2199
2200#[inline(always)]
2201pub(crate) fn exec_activation_backward(t: &Thunk, base: *mut u8) {
2202    let Thunk::ActivationBackward {
2203        x,
2204        dy,
2205        dx,
2206        len,
2207        kind,
2208    } = t
2209    else {
2210        unreachable!()
2211    };
2212    {
2213        let len = *len as usize;
2214        unsafe {
2215            let xs = sl(*x, base, len);
2216            let dys = sl(*dy, base, len);
2217            let out = sl_mut(*dx, base, len);
2218            activation_backward_kernel(*kind, xs, dys, out);
2219        }
2220    }
2221}
2222
2223#[inline(always)]
2224pub(crate) fn exec_activation_backward_f64(t: &Thunk, base: *mut u8) {
2225    let Thunk::ActivationBackwardF64 {
2226        x,
2227        dy,
2228        dx,
2229        len,
2230        kind,
2231    } = t
2232    else {
2233        unreachable!()
2234    };
2235    {
2236        let len = *len as usize;
2237        unsafe {
2238            let xs = sl_f64(*x, base, len);
2239            let dys = sl_f64(*dy, base, len);
2240            let out = sl_mut_f64(*dx, base, len);
2241            activation_backward_kernel_f64(*kind, xs, dys, out);
2242        }
2243    }
2244}
2245
2246#[inline(always)]
2247pub(crate) fn exec_gather_backward(t: &Thunk, base: *mut u8) {
2248    let Thunk::GatherBackward {
2249        dy,
2250        indices,
2251        dst,
2252        outer,
2253        axis_dim,
2254        num_idx,
2255        trailing,
2256    } = t
2257    else {
2258        unreachable!()
2259    };
2260    {
2261        let (outer, axis_dim, num_idx, trailing) = (
2262            *outer as usize,
2263            *axis_dim as usize,
2264            *num_idx as usize,
2265            *trailing as usize,
2266        );
2267        unsafe {
2268            let dys = sl(*dy, base, outer * num_idx * trailing);
2269            let ids = sl(*indices, base, num_idx);
2270            let out = sl_mut(*dst, base, outer * axis_dim * trailing);
2271            for v in out.iter_mut() {
2272                *v = 0.0;
2273            }
2274            crate::training_bwd::gather_axis_backward(
2275                dys, ids, out, outer, axis_dim, num_idx, trailing,
2276            );
2277        }
2278    }
2279}
2280
2281#[inline(always)]
2282pub(crate) fn exec_gather_axis(t: &Thunk, base: *mut u8) {
2283    let Thunk::GatherAxis {
2284        table,
2285        idx,
2286        dst,
2287        outer,
2288        axis_dim,
2289        num_idx,
2290        trailing,
2291        idx_i64,
2292        table_bytes,
2293    } = t
2294    else {
2295        unreachable!()
2296    };
2297    {
2298        let outer = *outer as usize;
2299        let axis_dim = *axis_dim as usize;
2300        let num_idx = *num_idx as usize;
2301        let trailing = *trailing as usize;
2302        unsafe {
2303            if *table_bytes == 8 {
2304                let tab = sl_i64(*table, base, outer * axis_dim * trailing);
2305                let out = sl_mut_i64(*dst, base, outer * num_idx * trailing);
2306                for o in 0..outer {
2307                    let tab_outer = o * axis_dim * trailing;
2308                    let out_outer = o * num_idx * trailing;
2309                    if *idx_i64 != 0 {
2310                        let ids = sl_i64(*idx, base, num_idx);
2311                        for k in 0..num_idx {
2312                            let row = ids[k].max(0) as usize;
2313                            if row < axis_dim {
2314                                let tab_row = tab_outer + row * trailing;
2315                                let out_row = out_outer + k * trailing;
2316                                out[out_row..out_row + trailing]
2317                                    .copy_from_slice(&tab[tab_row..tab_row + trailing]);
2318                            }
2319                        }
2320                    } else {
2321                        let ids = sl(*idx, base, num_idx);
2322                        for k in 0..num_idx {
2323                            let row = ids[k] as usize;
2324                            if row < axis_dim {
2325                                let tab_row = tab_outer + row * trailing;
2326                                let out_row = out_outer + k * trailing;
2327                                out[out_row..out_row + trailing]
2328                                    .copy_from_slice(&tab[tab_row..tab_row + trailing]);
2329                            }
2330                        }
2331                    }
2332                }
2333            } else {
2334                let tab = sl(*table, base, outer * axis_dim * trailing);
2335                let out = sl_mut(*dst, base, outer * num_idx * trailing);
2336                for o in 0..outer {
2337                    let tab_outer = o * axis_dim * trailing;
2338                    let out_outer = o * num_idx * trailing;
2339                    if *idx_i64 != 0 {
2340                        let ids = sl_i64(*idx, base, num_idx);
2341                        for k in 0..num_idx {
2342                            let row = ids[k].max(0) as usize;
2343                            if row < axis_dim {
2344                                let tab_row = tab_outer + row * trailing;
2345                                let out_row = out_outer + k * trailing;
2346                                out[out_row..out_row + trailing]
2347                                    .copy_from_slice(&tab[tab_row..tab_row + trailing]);
2348                            }
2349                        }
2350                    } else {
2351                        let ids = sl(*idx, base, num_idx);
2352                        for k in 0..num_idx {
2353                            let row = ids[k] as usize;
2354                            if row < axis_dim {
2355                                let tab_row = tab_outer + row * trailing;
2356                                let out_row = out_outer + k * trailing;
2357                                out[out_row..out_row + trailing]
2358                                    .copy_from_slice(&tab[tab_row..tab_row + trailing]);
2359                            }
2360                        }
2361                    }
2362                }
2363            }
2364        }
2365    }
2366}
2367
2368#[inline(always)]
2369pub(crate) fn exec_reverse(t: &Thunk, base: *mut u8) {
2370    let Thunk::Reverse {
2371        src,
2372        dst,
2373        dims,
2374        rev_mask,
2375        elem_bytes,
2376    } = t
2377    else {
2378        unreachable!()
2379    };
2380    {
2381        let eb = *elem_bytes as usize;
2382        let rank = dims.len();
2383        let total: usize = dims.iter().map(|&d| d as usize).product::<usize>().max(1);
2384        let mut strides = vec![1usize; rank];
2385        for i in (0..rank.saturating_sub(1)).rev() {
2386            strides[i] = strides[i + 1] * dims[i + 1] as usize;
2387        }
2388        unsafe {
2389            let src_base = base.add(*src);
2390            let dst_base = base.add(*dst);
2391            for o in 0..total {
2392                let mut rem = o;
2393                let mut in_flat = 0usize;
2394                for ax in 0..rank {
2395                    let idx = rem / strides[ax];
2396                    rem %= strides[ax];
2397                    let in_idx = if rev_mask[ax] {
2398                        dims[ax] as usize - 1 - idx
2399                    } else {
2400                        idx
2401                    };
2402                    in_flat += in_idx * strides[ax];
2403                }
2404                std::ptr::copy_nonoverlapping(src_base.add(in_flat * eb), dst_base.add(o * eb), eb);
2405            }
2406        }
2407    }
2408}
2409
2410/// Host-fallback entry for `Op::GatedDeltaNet` (Metal / unified memory).
2411/// When `state == 0`, uses a zero-initialized scratch state per batch item.
2412/// Batch-general reverse/flip (dtype-agnostic, byte-copy). Shared by the CPU
2413/// `Thunk::Reverse` arm and the Metal/WGPU host paths. `dims` is the row-major
2414/// input shape; `rev_mask[a]` flips axis `a`. `src`/`dst` are byte offsets.
2415pub unsafe fn execute_reverse(
2416    src: usize,
2417    dst: usize,
2418    dims: &[u32],
2419    rev_mask: &[bool],
2420    elem_bytes: usize,
2421    base: *mut u8,
2422) {
2423    let rank = dims.len();
2424    let total: usize = dims.iter().map(|&d| d as usize).product::<usize>().max(1);
2425    let mut strides = vec![1usize; rank];
2426    for i in (0..rank.saturating_sub(1)).rev() {
2427        strides[i] = strides[i + 1] * dims[i + 1] as usize;
2428    }
2429    unsafe {
2430        let src_base = base.add(src);
2431        let dst_base = base.add(dst);
2432        for o in 0..total {
2433            let mut rem = o;
2434            let mut in_flat = 0usize;
2435            for ax in 0..rank {
2436                let idx = rem / strides[ax];
2437                rem %= strides[ax];
2438                let in_idx = if rev_mask[ax] {
2439                    dims[ax] as usize - 1 - idx
2440                } else {
2441                    idx
2442                };
2443                in_flat += in_idx * strides[ax];
2444            }
2445            std::ptr::copy_nonoverlapping(
2446                src_base.add(in_flat * elem_bytes),
2447                dst_base.add(o * elem_bytes),
2448                elem_bytes,
2449            );
2450        }
2451    }
2452}
2453
2454/// Output-indexed pad over the shared arena (mirrors [`execute_reverse`]).
2455/// Every mode is a gather from `src` to `dst`; only `Constant` positions —
2456/// where some axis coordinate falls outside the input — write `fill` (the
2457/// constant value pre-encoded in the output dtype, `elem_bytes` long).
2458///
2459/// `before[i]`/`after[i]` are the pad widths on axis `i`; the output extent is
2460/// `in_dims[i] + before[i] + after[i]`. Byte-generic like `execute_reverse`.
2461#[allow(clippy::too_many_arguments)]
2462pub unsafe fn execute_pad(
2463    src: usize,
2464    dst: usize,
2465    in_dims: &[u32],
2466    before: &[u32],
2467    after: &[u32],
2468    mode: rlx_ir::PadMode,
2469    fill: &[u8],
2470    elem_bytes: usize,
2471    base: *mut u8,
2472) {
2473    use rlx_ir::PadMode;
2474    let rank = in_dims.len();
2475    let in_dims: Vec<usize> = in_dims.iter().map(|&d| d as usize).collect();
2476    let out_dims: Vec<usize> = (0..rank)
2477        .map(|i| in_dims[i] + before[i] as usize + after[i] as usize)
2478        .collect();
2479    let total: usize = out_dims.iter().product::<usize>().max(1);
2480    let mut in_strides = vec![1usize; rank];
2481    let mut out_strides = vec![1usize; rank];
2482    for i in (0..rank.saturating_sub(1)).rev() {
2483        in_strides[i] = in_strides[i + 1] * in_dims[i + 1];
2484        out_strides[i] = out_strides[i + 1] * out_dims[i + 1];
2485    }
2486    unsafe {
2487        let src_base = base.add(src);
2488        let dst_base = base.add(dst);
2489        for o in 0..total {
2490            let mut rem = o;
2491            let mut in_flat = 0usize;
2492            let mut is_fill = false;
2493            for ax in 0..rank {
2494                let oc = (rem / out_strides[ax]) as isize;
2495                rem %= out_strides[ax];
2496                // Unpadded axis: identity (also avoids the reflect period=0 case).
2497                if before[ax] == 0 && after[ax] == 0 {
2498                    in_flat += oc as usize * in_strides[ax];
2499                    continue;
2500                }
2501                let n = in_dims[ax] as isize;
2502                let p = oc - before[ax] as isize;
2503                let ic: isize = match mode {
2504                    PadMode::Constant(_) => {
2505                        if p < 0 || p >= n {
2506                            is_fill = true;
2507                            break;
2508                        }
2509                        p
2510                    }
2511                    PadMode::Replicate => p.clamp(0, n - 1),
2512                    PadMode::Circular => ((p % n) + n) % n,
2513                    PadMode::Reflect => {
2514                        let period = 2 * (n - 1);
2515                        let mut i = ((p % period) + period) % period;
2516                        if i >= n {
2517                            i = period - i;
2518                        }
2519                        i
2520                    }
2521                };
2522                in_flat += ic as usize * in_strides[ax];
2523            }
2524            let dptr = dst_base.add(o * elem_bytes);
2525            if is_fill {
2526                std::ptr::copy_nonoverlapping(fill.as_ptr(), dptr, elem_bytes);
2527            } else {
2528                std::ptr::copy_nonoverlapping(src_base.add(in_flat * elem_bytes), dptr, elem_bytes);
2529            }
2530        }
2531    }
2532}
2533
2534/// Output-indexed strided slice over the shared arena (mirrors
2535/// [`execute_reverse`]). `out[..,j,..] = in[.., start + j*step, ..]` along
2536/// `axis` (`step` may be negative). Byte-generic.
2537#[allow(clippy::too_many_arguments)]
2538pub unsafe fn execute_slice(
2539    src: usize,
2540    dst: usize,
2541    in_dims: &[u32],
2542    axis: usize,
2543    start: usize,
2544    len: usize,
2545    step: i64,
2546    elem_bytes: usize,
2547    base: *mut u8,
2548) {
2549    let rank = in_dims.len();
2550    let in_dims: Vec<usize> = in_dims.iter().map(|&d| d as usize).collect();
2551    let mut out_dims = in_dims.clone();
2552    out_dims[axis] = len;
2553    let total: usize = out_dims.iter().product::<usize>().max(1);
2554    let mut in_strides = vec![1usize; rank];
2555    let mut out_strides = vec![1usize; rank];
2556    for i in (0..rank.saturating_sub(1)).rev() {
2557        in_strides[i] = in_strides[i + 1] * in_dims[i + 1];
2558        out_strides[i] = out_strides[i + 1] * out_dims[i + 1];
2559    }
2560    unsafe {
2561        let src_base = base.add(src);
2562        let dst_base = base.add(dst);
2563        for o in 0..total {
2564            let mut rem = o;
2565            let mut in_flat = 0usize;
2566            for ax in 0..rank {
2567                let oc = rem / out_strides[ax];
2568                rem %= out_strides[ax];
2569                let ic = if ax == axis {
2570                    (start as i64 + oc as i64 * step) as usize
2571                } else {
2572                    oc
2573                };
2574                in_flat += ic * in_strides[ax];
2575            }
2576            std::ptr::copy_nonoverlapping(
2577                src_base.add(in_flat * elem_bytes),
2578                dst_base.add(o * elem_bytes),
2579                elem_bytes,
2580            );
2581        }
2582    }
2583}
2584
2585pub unsafe fn execute_gather_backward_f32(
2586    dy: usize,
2587    indices: usize,
2588    dst: usize,
2589    outer: u32,
2590    axis_dim: u32,
2591    num_idx: u32,
2592    trailing: u32,
2593    base: *mut u8,
2594) {
2595    let (outer, axis_dim, num_idx, trailing) = (
2596        outer as usize,
2597        axis_dim as usize,
2598        num_idx as usize,
2599        trailing as usize,
2600    );
2601    let out = sl_mut(dst, base, outer * axis_dim * trailing);
2602    out.fill(0.0);
2603    crate::training_bwd::gather_axis_backward(
2604        sl(dy, base, outer * num_idx * trailing),
2605        sl(indices, base, num_idx),
2606        out,
2607        outer,
2608        axis_dim,
2609        num_idx,
2610        trailing,
2611    );
2612}
2613
2614/// Scalar activation for the fused-region interpreter. Matches the GPU region
2615/// kernel's math (e.g. tanh-approx GELU) so CPU/GPU region results agree.
2616#[inline]
2617pub(crate) fn region_activation_scalar(act: rlx_ir::op::Activation, x: f32) -> f32 {
2618    use rlx_ir::op::Activation as A;
2619    const GC: f32 = 0.797_884_6; // sqrt(2/pi)
2620    match act {
2621        A::Relu => x.max(0.0),
2622        A::Gelu | A::GeluApprox => 0.5 * x * (1.0 + (GC * (x + 0.044715 * x * x * x)).tanh()),
2623        A::Silu => x / (1.0 + (-x).exp()),
2624        A::Sigmoid => 1.0 / (1.0 + (-x).exp()),
2625        A::Tanh => x.tanh(),
2626        A::Exp => x.exp(),
2627        A::Log => x.ln(),
2628        A::Sqrt => x.sqrt(),
2629        A::Rsqrt => 1.0 / x.sqrt(),
2630        A::Neg => -x,
2631        A::Abs => x.abs(),
2632        A::Sin => x.sin(),
2633        A::Cos => x.cos(),
2634        A::Tan => x.tan(),
2635        A::Atan => x.atan(),
2636        A::Recip => 1.0 / x,
2637        A::Round => x.round(),
2638        A::Floor => x.floor(),
2639        A::Ceil => x.ceil(),
2640        A::Sign => {
2641            if x > 0.0 {
2642                1.0
2643            } else if x < 0.0 {
2644                -1.0
2645            } else {
2646                0.0
2647            }
2648        }
2649        A::Softplus => x.max(0.0) + (-(x.abs())).exp().ln_1p(),
2650        A::Elu => {
2651            if x > 0.0 {
2652                x
2653            } else {
2654                x.exp() - 1.0
2655            }
2656        }
2657        A::Erf => erf_f32(x),
2658        A::HardSwish => x * (x + 3.0).clamp(0.0, 6.0) / 6.0,
2659        A::HardSigmoid => (x / 6.0 + 0.5).clamp(0.0, 1.0),
2660        A::Mish => x * (x.max(0.0) + (-(x.abs())).exp().ln_1p()).tanh(),
2661        A::Softsign => x / (1.0 + x.abs()),
2662        A::LogSigmoid => x.min(0.0) - (-(x.abs())).exp().ln_1p(),
2663    }
2664}
2665
2666#[inline]
2667pub(crate) fn region_binary_scalar(op: rlx_ir::op::BinaryOp, l: f32, r: f32) -> f32 {
2668    use rlx_ir::op::BinaryOp as B;
2669    match op {
2670        B::Add => l + r,
2671        B::Sub => l - r,
2672        B::Mul => l * r,
2673        B::Div => l / r,
2674        B::Max => l.max(r),
2675        B::Min => l.min(r),
2676        B::Pow => l.powf(r),
2677        B::Mod => l % r,
2678        B::Atan2 => l.atan2(r),
2679        B::BitAnd => ((l as i64) & (r as i64)) as f32,
2680        B::BitOr => ((l as i64) | (r as i64)) as f32,
2681        B::BitXor => ((l as i64) ^ (r as i64)) as f32,
2682        B::Shl => (l as i64).wrapping_shl(r as u32) as f32,
2683        B::Shr => (l as i64).wrapping_shr(r as u32) as f32,
2684    }
2685}
2686
2687#[inline]
2688pub(crate) fn region_compare_scalar(op: rlx_ir::op::CmpOp, l: f32, r: f32) -> bool {
2689    use rlx_ir::op::CmpOp as C;
2690    match op {
2691        C::Eq => l == r,
2692        C::Ne => l != r,
2693        C::Lt => l < r,
2694        C::Le => l <= r,
2695        C::Gt => l > r,
2696        C::Ge => l >= r,
2697    }
2698}
2699
2700/// Resolve one chain operand for output element `gid`: a previous step result
2701/// (`scratch[s]`) or an external input read with broadcast (`input[gid % mod]`,
2702/// scalar inputs read element 0). `base` is the arena byte base.
2703#[inline]
2704pub(crate) fn region_resolve_operand(
2705    op: &rlx_ir::op::ChainOperand,
2706    gid: usize,
2707    base: *const u8,
2708    input_offs: &[usize],
2709    scalar_mask: u32,
2710    modulus: &[u32; 16],
2711    scratch: &[f32; 32],
2712) -> f32 {
2713    use rlx_ir::op::ChainOperand as O;
2714    match op {
2715        O::Step(s) => scratch[*s as usize],
2716        O::Input(i) => {
2717            let i = *i as usize;
2718            let row = if (scalar_mask >> i) & 1 == 1 {
2719                0
2720            } else if modulus[i] != 0 {
2721                gid % modulus[i] as usize
2722            } else {
2723                gid
2724            };
2725            unsafe { *(base.add(input_offs[i]) as *const f32).add(row) }
2726        }
2727    }
2728}
2729
2730/// Evaluate a fused element-wise chain for output element `gid`, returning the
2731/// final step's value (what gets written to `dst[gid]`).
2732#[inline]
2733pub(crate) fn region_eval_elem(
2734    gid: usize,
2735    base: *const u8,
2736    input_offs: &[usize],
2737    chain: &[rlx_ir::op::ChainStep],
2738    scalar_mask: u32,
2739    modulus: &[u32; 16],
2740) -> f32 {
2741    use rlx_ir::op::ChainStep as S;
2742    let mut scratch = [0f32; 32];
2743    let r = |o: &rlx_ir::op::ChainOperand, sc: &[f32; 32]| {
2744        region_resolve_operand(o, gid, base, input_offs, scalar_mask, modulus, sc)
2745    };
2746    for (k, step) in chain.iter().enumerate() {
2747        scratch[k] = match step {
2748            S::Activation(a, x) => region_activation_scalar(*a, r(x, &scratch)),
2749            S::Cast(_, x) => r(x, &scratch), // f32→f32 identity (chains are same-dtype)
2750            S::Binary(op, l, rr) => region_binary_scalar(*op, r(l, &scratch), r(rr, &scratch)),
2751            S::Compare(op, l, rr) => {
2752                if region_compare_scalar(*op, r(l, &scratch), r(rr, &scratch)) {
2753                    1.0
2754                } else {
2755                    0.0
2756                }
2757            }
2758            S::Where(c, t, f) => {
2759                if r(c, &scratch) != 0.0 {
2760                    r(t, &scratch)
2761                } else {
2762                    r(f, &scratch)
2763                }
2764            }
2765        };
2766    }
2767    scratch[chain.len() - 1]
2768}
2769
2770// Unsafe helpers to create slices from arena base + offset
2771/// In-place per-element activation. Mirrors the dispatch in
2772/// `Thunk::ActivationInPlace`. Used by `Thunk::FusedMmBiasAct` to
2773/// apply the activation after `bias_add` for all non-Gelu cases.
2774#[inline(always)]
2775pub(crate) fn apply_activation_inplace(d: &mut [f32], act: rlx_ir::op::Activation) {
2776    use rlx_ir::op::Activation;
2777    match act {
2778        Activation::Gelu => crate::kernels::par_gelu_inplace(d),
2779        Activation::GeluApprox => crate::kernels::par_gelu_approx_inplace(d),
2780        Activation::Silu => crate::kernels::par_silu_inplace(d),
2781        Activation::Relu => {
2782            for v in d.iter_mut() {
2783                *v = v.max(0.0);
2784            }
2785        }
2786        Activation::Sigmoid => {
2787            let x = d.to_vec();
2788            crate::vmath::vvsigmoidf(d, &x);
2789        }
2790        Activation::Tanh => crate::vmath::vvtanhf_hot_inplace(d),
2791        Activation::Exp => crate::vmath::vvexpf_hot_inplace(d),
2792        Activation::Log => crate::vmath::vvlogf_inplace(d),
2793        Activation::Sqrt => crate::vmath::vvsqrtf_inplace(d),
2794        Activation::Rsqrt => crate::vmath::vvrsqrtf_inplace(d),
2795        Activation::Neg => {
2796            for v in d.iter_mut() {
2797                *v = -*v;
2798            }
2799        }
2800        Activation::Abs => {
2801            for v in d.iter_mut() {
2802                *v = v.abs();
2803            }
2804        }
2805        Activation::Round => {
2806            for v in d.iter_mut() {
2807                *v = v.round();
2808            }
2809        }
2810        Activation::Sin => {
2811            for v in d.iter_mut() {
2812                *v = v.sin();
2813            }
2814        }
2815        Activation::Cos => {
2816            for v in d.iter_mut() {
2817                *v = v.cos();
2818            }
2819        }
2820        Activation::Tan => {
2821            for v in d.iter_mut() {
2822                *v = v.tan();
2823            }
2824        }
2825        Activation::Atan => {
2826            for v in d.iter_mut() {
2827                *v = v.atan();
2828            }
2829        }
2830        Activation::Recip => crate::vmath::vvrecf_inplace(d),
2831        Activation::Floor => {
2832            for v in d.iter_mut() {
2833                *v = v.floor();
2834            }
2835        }
2836        Activation::Ceil => {
2837            for v in d.iter_mut() {
2838                *v = v.ceil();
2839            }
2840        }
2841        Activation::Sign => {
2842            for v in d.iter_mut() {
2843                *v = if *v > 0.0 {
2844                    1.0
2845                } else if *v < 0.0 {
2846                    -1.0
2847                } else {
2848                    0.0
2849                };
2850            }
2851        }
2852        Activation::Softplus => {
2853            for v in d.iter_mut() {
2854                *v = v.max(0.0) + (-(v.abs())).exp().ln_1p();
2855            }
2856        }
2857        Activation::Elu => {
2858            for v in d.iter_mut() {
2859                *v = if *v > 0.0 { *v } else { v.exp() - 1.0 };
2860            }
2861        }
2862        Activation::Erf => {
2863            for v in d.iter_mut() {
2864                *v = erf_f32(*v);
2865            }
2866        }
2867        Activation::HardSwish => {
2868            for v in d.iter_mut() {
2869                let x = *v;
2870                *v = x * (x + 3.0).clamp(0.0, 6.0) / 6.0;
2871            }
2872        }
2873        Activation::HardSigmoid => {
2874            for v in d.iter_mut() {
2875                *v = (*v / 6.0 + 0.5).clamp(0.0, 1.0);
2876            }
2877        }
2878        Activation::Mish => {
2879            for v in d.iter_mut() {
2880                let x = *v;
2881                *v = x * (x.max(0.0) + (-(x.abs())).exp().ln_1p()).tanh();
2882            }
2883        }
2884        Activation::Softsign => {
2885            for v in d.iter_mut() {
2886                let x = *v;
2887                *v = x / (1.0 + x.abs());
2888            }
2889        }
2890        Activation::LogSigmoid => {
2891            for v in d.iter_mut() {
2892                let x = *v;
2893                *v = x.min(0.0) - (-(x.abs())).exp().ln_1p();
2894            }
2895        }
2896    }
2897}
2898
2899pub(crate) fn activation_backward_kernel(
2900    act: rlx_ir::op::Activation,
2901    xs: &[f32],
2902    dys: &[f32],
2903    out: &mut [f32],
2904) {
2905    use rlx_ir::op::Activation;
2906    let n = xs.len();
2907    debug_assert_eq!(dys.len(), n);
2908    debug_assert_eq!(out.len(), n);
2909    match act {
2910        Activation::Relu => {
2911            for i in 0..n {
2912                out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
2913            }
2914        }
2915        Activation::Sigmoid => {
2916            for i in 0..n {
2917                let s = 1.0 / (1.0 + (-xs[i]).exp());
2918                out[i] = s * (1.0 - s) * dys[i];
2919            }
2920        }
2921        Activation::Tanh => {
2922            for i in 0..n {
2923                let t = xs[i].tanh();
2924                out[i] = (1.0 - t * t) * dys[i];
2925            }
2926        }
2927        Activation::Silu => {
2928            // y = x * σ(x);  dy/dx = σ(x) * (1 + x * (1 - σ(x))).
2929            for i in 0..n {
2930                let s = 1.0 / (1.0 + (-xs[i]).exp());
2931                out[i] = s * (1.0 + xs[i] * (1.0 - s)) * dys[i];
2932            }
2933        }
2934        Activation::Gelu => {
2935            // Exact erf-based GELU:  y = 0.5 x (1 + erf(x / √2)).
2936            //   dy/dx = 0.5 (1 + erf(x/√2)) + (x / √(2π)) · exp(-x²/2)
2937            const INV_SQRT2: f32 = 0.707_106_77;
2938            const INV_SQRT_2PI: f32 = 0.398_942_3;
2939            for i in 0..n {
2940                let x = xs[i];
2941                let phi = 0.5 * (1.0 + erf_f32(x * INV_SQRT2));
2942                let pdf = INV_SQRT_2PI * (-(x * x) * 0.5).exp();
2943                out[i] = (phi + x * pdf) * dys[i];
2944            }
2945        }
2946        Activation::GeluApprox => {
2947            // Tanh-approximation:
2948            //   y = 0.5 x (1 + tanh(c · (x + 0.044715 x³))) where c = √(2/π).
2949            const C: f32 = 0.797_884_6; // √(2/π)
2950            const A: f32 = 0.044_715;
2951            for i in 0..n {
2952                let x = xs[i];
2953                let inner = C * (x + A * x * x * x);
2954                let t = inner.tanh();
2955                let dinner = C * (1.0 + 3.0 * A * x * x);
2956                let d = 0.5 * (1.0 + t) + 0.5 * x * (1.0 - t * t) * dinner;
2957                out[i] = d * dys[i];
2958            }
2959        }
2960        Activation::Exp => {
2961            for i in 0..n {
2962                out[i] = xs[i].exp() * dys[i];
2963            }
2964        }
2965        Activation::Log => {
2966            for i in 0..n {
2967                out[i] = dys[i] / xs[i];
2968            }
2969        }
2970        Activation::Sqrt => {
2971            // d/dx √x = 0.5 / √x — undefined at x=0; clamp to 0.
2972            for i in 0..n {
2973                let s = xs[i].sqrt();
2974                out[i] = if s > 0.0 { 0.5 * dys[i] / s } else { 0.0 };
2975            }
2976        }
2977        Activation::Rsqrt => {
2978            // d/dx (1/√x) = -0.5 · x^(-3/2).
2979            for i in 0..n {
2980                let s = xs[i].sqrt();
2981                out[i] = if s > 0.0 {
2982                    -0.5 * dys[i] / (xs[i] * s)
2983                } else {
2984                    0.0
2985                };
2986            }
2987        }
2988        Activation::Neg => {
2989            for i in 0..n {
2990                out[i] = -dys[i];
2991            }
2992        }
2993        Activation::Abs => {
2994            // sign(x); 0 at x=0.
2995            for i in 0..n {
2996                let x = xs[i];
2997                let s = if x > 0.0 {
2998                    1.0
2999                } else if x < 0.0 {
3000                    -1.0
3001                } else {
3002                    0.0
3003                };
3004                out[i] = s * dys[i];
3005            }
3006        }
3007        Activation::Round => {
3008            // STE: pretend the round was identity in the backward
3009            // pass. The round step has zero gradient almost
3010            // everywhere, so without this trick the optimizer can't
3011            // learn through it.
3012            out.copy_from_slice(dys);
3013        }
3014        Activation::Sin => {
3015            // d/dx sin(x) = cos(x).
3016            for i in 0..n {
3017                out[i] = xs[i].cos() * dys[i];
3018            }
3019        }
3020        Activation::Cos => {
3021            for i in 0..n {
3022                out[i] = -xs[i].sin() * dys[i];
3023            }
3024        }
3025        Activation::Tan => {
3026            // d/dx tan(x) = sec²(x) = 1 + tan²(x)
3027            for i in 0..n {
3028                let t = xs[i].tan();
3029                out[i] = (1.0 + t * t) * dys[i];
3030            }
3031        }
3032        Activation::Atan => {
3033            // d/dx atan(x) = 1 / (1 + x²)
3034            for i in 0..n {
3035                let x = xs[i];
3036                out[i] = dys[i] / (1.0 + x * x);
3037            }
3038        }
3039        Activation::Recip => {
3040            for i in 0..n {
3041                out[i] = -dys[i] / (xs[i] * xs[i]);
3042            }
3043        }
3044        // Piecewise-constant: zero gradient.
3045        Activation::Floor | Activation::Ceil | Activation::Sign => {
3046            for o in out.iter_mut() {
3047                *o = 0.0;
3048            }
3049        }
3050        Activation::Softplus => {
3051            for i in 0..n {
3052                out[i] = dys[i] / (1.0 + (-xs[i]).exp());
3053            }
3054        }
3055        Activation::Elu => {
3056            for i in 0..n {
3057                out[i] = if xs[i] > 0.0 {
3058                    dys[i]
3059                } else {
3060                    dys[i] * xs[i].exp()
3061                };
3062            }
3063        }
3064        // Decomposed at the AD level (vjp_activation) — never reach this kernel.
3065        Activation::Erf
3066        | Activation::HardSwish
3067        | Activation::HardSigmoid
3068        | Activation::Mish
3069        | Activation::Softsign
3070        | Activation::LogSigmoid => {
3071            panic!("activation_backward_kernel: {act:?} is decomposed at the AD level")
3072        }
3073    }
3074}
3075
3076/// f64 sibling of `activation_backward_kernel`. Same math, twice the
3077/// precision — used by f64 graphs where the f32 kernel reading bytes
3078/// as `&[f32]` would silently discard half of every f64 value.
3079pub(crate) fn activation_backward_kernel_f64(
3080    act: rlx_ir::op::Activation,
3081    xs: &[f64],
3082    dys: &[f64],
3083    out: &mut [f64],
3084) {
3085    use rlx_ir::op::Activation;
3086    let n = xs.len();
3087    debug_assert_eq!(dys.len(), n);
3088    debug_assert_eq!(out.len(), n);
3089    match act {
3090        Activation::Relu => {
3091            for i in 0..n {
3092                out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
3093            }
3094        }
3095        Activation::Sigmoid => {
3096            for i in 0..n {
3097                let s = 1.0 / (1.0 + (-xs[i]).exp());
3098                out[i] = s * (1.0 - s) * dys[i];
3099            }
3100        }
3101        Activation::Tanh => {
3102            for i in 0..n {
3103                let t = xs[i].tanh();
3104                out[i] = (1.0 - t * t) * dys[i];
3105            }
3106        }
3107        Activation::Silu => {
3108            for i in 0..n {
3109                let s = 1.0 / (1.0 + (-xs[i]).exp());
3110                out[i] = s * (1.0 + xs[i] * (1.0 - s)) * dys[i];
3111            }
3112        }
3113        Activation::Gelu | Activation::GeluApprox => {
3114            // Both rare on f64 paths; use the high-quality libm erf.
3115            const INV_SQRT2: f64 = std::f64::consts::FRAC_1_SQRT_2;
3116            const INV_SQRT_2PI: f64 = 0.398_942_280_401_432_7;
3117            for i in 0..n {
3118                let x = xs[i];
3119                let phi = 0.5 * (1.0 + erf_f64(x * INV_SQRT2));
3120                let pdf = INV_SQRT_2PI * (-(x * x) * 0.5).exp();
3121                out[i] = (phi + x * pdf) * dys[i];
3122            }
3123        }
3124        Activation::Exp => {
3125            for i in 0..n {
3126                out[i] = xs[i].exp() * dys[i];
3127            }
3128        }
3129        Activation::Log => {
3130            for i in 0..n {
3131                out[i] = dys[i] / xs[i];
3132            }
3133        }
3134        Activation::Sqrt => {
3135            for i in 0..n {
3136                let s = xs[i].sqrt();
3137                out[i] = if s > 0.0 { 0.5 * dys[i] / s } else { 0.0 };
3138            }
3139        }
3140        Activation::Rsqrt => {
3141            for i in 0..n {
3142                let s = xs[i].sqrt();
3143                out[i] = if s > 0.0 {
3144                    -0.5 * dys[i] / (xs[i] * s)
3145                } else {
3146                    0.0
3147                };
3148            }
3149        }
3150        Activation::Neg => {
3151            for i in 0..n {
3152                out[i] = -dys[i];
3153            }
3154        }
3155        Activation::Abs => {
3156            for i in 0..n {
3157                let x = xs[i];
3158                let s = if x > 0.0 {
3159                    1.0
3160                } else if x < 0.0 {
3161                    -1.0
3162                } else {
3163                    0.0
3164                };
3165                out[i] = s * dys[i];
3166            }
3167        }
3168        Activation::Round => {
3169            out.copy_from_slice(dys);
3170        }
3171        Activation::Sin => {
3172            for i in 0..n {
3173                out[i] = xs[i].cos() * dys[i];
3174            }
3175        }
3176        Activation::Cos => {
3177            for i in 0..n {
3178                out[i] = -xs[i].sin() * dys[i];
3179            }
3180        }
3181        Activation::Tan => {
3182            for i in 0..n {
3183                let t = xs[i].tan();
3184                out[i] = (1.0 + t * t) * dys[i];
3185            }
3186        }
3187        Activation::Atan => {
3188            for i in 0..n {
3189                let x = xs[i];
3190                out[i] = dys[i] / (1.0 + x * x);
3191            }
3192        }
3193        Activation::Recip => {
3194            for i in 0..n {
3195                out[i] = -dys[i] / (xs[i] * xs[i]);
3196            }
3197        }
3198        // Piecewise-constant: zero gradient.
3199        Activation::Floor | Activation::Ceil | Activation::Sign => {
3200            for o in out.iter_mut() {
3201                *o = 0.0;
3202            }
3203        }
3204        Activation::Softplus => {
3205            for i in 0..n {
3206                out[i] = dys[i] / (1.0 + (-xs[i]).exp());
3207            }
3208        }
3209        Activation::Elu => {
3210            for i in 0..n {
3211                out[i] = if xs[i] > 0.0 {
3212                    dys[i]
3213                } else {
3214                    dys[i] * xs[i].exp()
3215                };
3216            }
3217        }
3218        // Decomposed at the AD level (vjp_activation) — never reach this kernel.
3219        Activation::Erf
3220        | Activation::HardSwish
3221        | Activation::HardSigmoid
3222        | Activation::Mish
3223        | Activation::Softsign
3224        | Activation::LogSigmoid => {
3225            panic!("activation_backward_kernel: {act:?} is decomposed at the AD level")
3226        }
3227    }
3228}
3229
3230/// f64 erf via A&S 7.1.26 — same coefficients as `erf_f32`, computed
3231/// at f64 width. Max error ~1.5e-7 (limited by the polynomial, not the
3232/// arithmetic). Adequate for gradient kernels; if higher precision is
3233/// needed, swap in a libm dependency.
3234#[inline(always)]
3235pub(crate) fn erf_f64(x: f64) -> f64 {
3236    let s = x.signum();
3237    let x = x.abs();
3238    let t = 1.0 / (1.0 + 0.327_591_1 * x);
3239    let y = 1.0
3240        - (((((1.061_405_43 * t - 1.453_152_03) * t) + 1.421_413_75) * t - 0.284_496_74) * t
3241            + 0.254_829_59)
3242            * t
3243            * (-x * x).exp();
3244    s * y
3245}
3246
3247/// Cheap erf approximation (Abramowitz & Stegun 7.1.26, max error ~1.5e-7
3248/// over all of ℝ — plenty for f32 gradient kernels).
3249#[inline(always)]
3250pub(crate) fn erf_f32(x: f32) -> f32 {
3251    let s = x.signum();
3252    let x = x.abs();
3253    let t = 1.0 / (1.0 + 0.327_591_1 * x);
3254    let y = 1.0
3255        - (((((1.061_405_4 * t - 1.453_152_1) * t) + 1.421_413_8) * t - 0.284_496_74) * t
3256            + 0.254_829_6)
3257            * t
3258            * (-x * x).exp();
3259    s * y
3260}
3261
3262pub(crate) fn narrow_thunk_closure(
3263    src: usize,
3264    dst: usize,
3265    outer: u32,
3266    src_stride: u32,
3267    dst_stride: u32,
3268    inner: u32,
3269    elem_bytes: u8,
3270) -> Arc<dyn Fn(*mut u8) + Send + Sync> {
3271    let (outer, ss, ds, inner, eb) = (
3272        outer as usize,
3273        src_stride as usize,
3274        dst_stride as usize,
3275        inner as usize,
3276        elem_bytes as usize,
3277    );
3278    let row_bytes = inner.saturating_mul(eb);
3279    let src_row_stride = ss.saturating_mul(eb);
3280    let dst_row_stride = ds.saturating_mul(eb);
3281    Arc::new(move |base: *mut u8| unsafe {
3282        if row_bytes == 0 || src == dst {
3283            return;
3284        }
3285        // Compiled-fn path has no arena length; skip if offsets look bogus.
3286        let arena_len = usize::MAX;
3287        for o in 0..outer {
3288            let s_off = src + o * src_row_stride;
3289            let d_off = dst + o * dst_row_stride;
3290            if s_off == d_off {
3291                continue;
3292            }
3293            if s_off.saturating_add(row_bytes) > arena_len
3294                || d_off.saturating_add(row_bytes) > arena_len
3295            {
3296                break;
3297            }
3298            std::ptr::copy_nonoverlapping(base.add(s_off), base.add(d_off), row_bytes);
3299        }
3300    })
3301}
3302
3303/// f64 N-D index walk used by Transpose and Expand. `out_dims` gives
3304/// the output shape; `in_strides` gives the source stride for each
3305/// output dim (broadcast axes have stride 0).
3306pub(crate) fn transpose_walk_f64(
3307    inp: &[f64],
3308    out: &mut [f64],
3309    out_dims: &[u32],
3310    in_strides: &[u32],
3311) {
3312    let rank = out_dims.len();
3313    let mut idx = vec![0u32; rank];
3314    for o in 0..out.len() {
3315        let mut src_off = 0usize;
3316        for d in 0..rank {
3317            src_off += idx[d] as usize * in_strides[d] as usize;
3318        }
3319        out[o] = inp[broadcast_src_index(src_off, inp.len())];
3320        // Increment index — last dim varies fastest.
3321        for d in (0..rank).rev() {
3322            idx[d] += 1;
3323            if idx[d] < out_dims[d] {
3324                break;
3325            }
3326            idx[d] = 0;
3327        }
3328    }
3329}
3330
3331/// f64 elementwise activation. Reads `inp`, writes `out`. For now
3332/// covers what the autodiff-emitted gradient graph needs (Neg, Exp,
3333/// Log, Sqrt, Rsqrt, Abs, Tanh, Sigmoid, Relu — the
3334/// transcendental-free subset). Approximate Gelu/Silu deferred until a
3335/// workload demands them at f64.
3336pub(crate) fn apply_activation_f64(inp: &[f64], out: &mut [f64], kind: Activation) {
3337    match kind {
3338        Activation::Neg => {
3339            for (o, &v) in out.iter_mut().zip(inp) {
3340                *o = -v;
3341            }
3342        }
3343        Activation::Exp => {
3344            for (o, &v) in out.iter_mut().zip(inp) {
3345                *o = v.exp();
3346            }
3347        }
3348        Activation::Log => {
3349            for (o, &v) in out.iter_mut().zip(inp) {
3350                *o = v.ln();
3351            }
3352        }
3353        Activation::Sqrt => {
3354            for (o, &v) in out.iter_mut().zip(inp) {
3355                *o = v.sqrt();
3356            }
3357        }
3358        Activation::Rsqrt => {
3359            for (o, &v) in out.iter_mut().zip(inp) {
3360                *o = 1.0 / v.sqrt();
3361            }
3362        }
3363        Activation::Abs => {
3364            for (o, &v) in out.iter_mut().zip(inp) {
3365                *o = v.abs();
3366            }
3367        }
3368        Activation::Tanh => {
3369            for (o, &v) in out.iter_mut().zip(inp) {
3370                *o = v.tanh();
3371            }
3372        }
3373        Activation::Sigmoid => {
3374            for (o, &v) in out.iter_mut().zip(inp) {
3375                *o = 1.0 / (1.0 + (-v).exp());
3376            }
3377        }
3378        Activation::Relu => {
3379            for (o, &v) in out.iter_mut().zip(inp) {
3380                *o = v.max(0.0);
3381            }
3382        }
3383        Activation::Round => {
3384            for (o, &v) in out.iter_mut().zip(inp) {
3385                *o = v.round_ties_even();
3386            }
3387        }
3388        Activation::Sin => {
3389            for (o, &v) in out.iter_mut().zip(inp) {
3390                *o = v.sin();
3391            }
3392        }
3393        Activation::Cos => {
3394            for (o, &v) in out.iter_mut().zip(inp) {
3395                *o = v.cos();
3396            }
3397        }
3398        Activation::Tan => {
3399            for (o, &v) in out.iter_mut().zip(inp) {
3400                *o = v.tan();
3401            }
3402        }
3403        Activation::Atan => {
3404            for (o, &v) in out.iter_mut().zip(inp) {
3405                *o = v.atan();
3406            }
3407        }
3408        Activation::Recip => {
3409            for (o, &v) in out.iter_mut().zip(inp) {
3410                *o = 1.0 / v;
3411            }
3412        }
3413        Activation::Floor => {
3414            for (o, &v) in out.iter_mut().zip(inp) {
3415                *o = v.floor();
3416            }
3417        }
3418        Activation::Ceil => {
3419            for (o, &v) in out.iter_mut().zip(inp) {
3420                *o = v.ceil();
3421            }
3422        }
3423        Activation::Sign => {
3424            for (o, &v) in out.iter_mut().zip(inp) {
3425                *o = if v > 0.0 {
3426                    1.0
3427                } else if v < 0.0 {
3428                    -1.0
3429                } else {
3430                    0.0
3431                };
3432            }
3433        }
3434        Activation::Softplus => {
3435            for (o, &v) in out.iter_mut().zip(inp) {
3436                *o = v.max(0.0) + (-(v.abs())).exp().ln_1p();
3437            }
3438        }
3439        Activation::Elu => {
3440            for (o, &v) in out.iter_mut().zip(inp) {
3441                *o = if v > 0.0 { v } else { v.exp() - 1.0 };
3442            }
3443        }
3444        Activation::Erf => {
3445            for (o, &v) in out.iter_mut().zip(inp) {
3446                *o = erf_f64(v);
3447            }
3448        }
3449        Activation::HardSwish => {
3450            for (o, &v) in out.iter_mut().zip(inp) {
3451                *o = v * (v + 3.0).clamp(0.0, 6.0) / 6.0;
3452            }
3453        }
3454        Activation::HardSigmoid => {
3455            for (o, &v) in out.iter_mut().zip(inp) {
3456                *o = (v / 6.0 + 0.5).clamp(0.0, 1.0);
3457            }
3458        }
3459        Activation::Mish => {
3460            for (o, &v) in out.iter_mut().zip(inp) {
3461                *o = v * (v.max(0.0) + (-(v.abs())).exp().ln_1p()).tanh();
3462            }
3463        }
3464        Activation::Softsign => {
3465            for (o, &v) in out.iter_mut().zip(inp) {
3466                *o = v / (1.0 + v.abs());
3467            }
3468        }
3469        Activation::LogSigmoid => {
3470            for (o, &v) in out.iter_mut().zip(inp) {
3471                *o = v.min(0.0) - (-(v.abs())).exp().ln_1p();
3472            }
3473        }
3474        Activation::Gelu | Activation::GeluApprox | Activation::Silu => {
3475            panic!(
3476                "apply_activation_f64: {kind:?} not yet implemented at f64. \
3477                    Add when a workload needs it."
3478            );
3479        }
3480    }
3481}
3482
3483#[inline]
3484pub(crate) fn binary_op_f64(op: BinaryOp, a: f64, b: f64) -> f64 {
3485    match op {
3486        BinaryOp::Add => a + b,
3487        BinaryOp::Sub => a - b,
3488        BinaryOp::Mul => a * b,
3489        BinaryOp::Div => a / b,
3490        BinaryOp::Max => a.max(b),
3491        BinaryOp::Min => a.min(b),
3492        BinaryOp::Pow => a.powf(b),
3493        BinaryOp::Mod => a % b,
3494        BinaryOp::Atan2 => a.atan2(b),
3495        BinaryOp::BitAnd => ((a as i64) & (b as i64)) as f64,
3496        BinaryOp::BitOr => ((a as i64) | (b as i64)) as f64,
3497        BinaryOp::BitXor => ((a as i64) ^ (b as i64)) as f64,
3498        BinaryOp::Shl => ((a as i64) << (b as i64)) as f64,
3499        BinaryOp::Shr => ((a as i64) >> (b as i64)) as f64,
3500    }
3501}