Skip to main content

rlx_cpu/thunk/ops/
elementwise.rs

1#![allow(unsafe_op_in_unsafe_fn)]
2use crate::thunk::*;
3
4#[allow(unused_variables)]
5pub(crate) fn compile_gather(
6    node: &rlx_ir::Node,
7    graph: &Graph,
8    arena: &crate::arena::Arena,
9    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
10    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
11    rng: rlx_ir::RngOptions,
12) -> Thunk {
13    let Op::Gather { axis } = &node.op else {
14        unreachable!()
15    };
16    {
17        // Non-zero axis: outer × num_idx × trailing layout.
18        let table_shape = &graph.node(node.inputs[0]).shape;
19        let rank = table_shape.rank();
20        let outer: usize = (0..*axis)
21            .map(|i| table_shape.dim(i).unwrap_static())
22            .product::<usize>()
23            .max(1);
24        let trailing: usize = (*axis + 1..rank)
25            .map(|i| table_shape.dim(i).unwrap_static())
26            .product::<usize>()
27            .max(1);
28        let axis_dim = table_shape.dim(*axis).unwrap_static();
29        let idx_len = get_len(graph, node.inputs[1]);
30        let idx_i64 = u8::from(graph.node(node.inputs[1]).shape.dtype() == rlx_ir::DType::I64);
31        let table_bytes = graph.node(node.inputs[0]).shape.dtype().size_bytes() as u8;
32        Thunk::GatherAxis {
33            table: node_offset(arena, node.inputs[0]),
34            idx: node_offset(arena, node.inputs[1]),
35            dst: node_offset(arena, node.id),
36            outer: outer as u32,
37            axis_dim: axis_dim as u32,
38            num_idx: idx_len as u32,
39            trailing: trailing as u32,
40            idx_i64,
41            table_bytes,
42        }
43    }
44}
45
46#[allow(unused_variables)]
47pub(crate) fn compile_narrow(
48    node: &rlx_ir::Node,
49    graph: &Graph,
50    arena: &crate::arena::Arena,
51    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
52    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
53    rng: rlx_ir::RngOptions,
54) -> Thunk {
55    let Op::Narrow { axis, start, len } = &node.op else {
56        unreachable!()
57    };
58    {
59        let in_shape = &graph.node(node.inputs[0]).shape;
60        let elem_bytes = in_shape.dtype().size_bytes() as u8;
61        let rank = in_shape.rank();
62        let outer: usize = (0..*axis)
63            .map(|i| in_shape.dim(i).unwrap_static())
64            .product::<usize>()
65            .max(1);
66        let inner: usize = (*axis + 1..rank)
67            .map(|i| in_shape.dim(i).unwrap_static())
68            .product::<usize>()
69            .max(1);
70        let in_axis = in_shape.dim(*axis).unwrap_static();
71        let src_byte_offset =
72            node_offset(arena, node.inputs[0]) + start * inner * elem_bytes as usize;
73        Thunk::Narrow {
74            src: src_byte_offset,
75            dst: node_offset(arena, node.id),
76            outer: outer as u32,
77            src_stride: (in_axis * inner) as u32, // elements per outer step in source
78            dst_stride: (*len * inner) as u32,    // elements per outer step in dest
79            inner: (*len * inner) as u32,         // elements to copy per outer step
80            elem_bytes,
81        }
82    }
83}
84
85#[allow(unused_variables)]
86pub(crate) fn compile_reverse(
87    node: &rlx_ir::Node,
88    graph: &Graph,
89    arena: &crate::arena::Arena,
90    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
91    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
92    rng: rlx_ir::RngOptions,
93) -> Thunk {
94    let Op::Reverse { axes } = &node.op else {
95        unreachable!()
96    };
97    {
98        let in_shape = &graph.node(node.inputs[0]).shape;
99        let rank = in_shape.rank();
100        let dims: Vec<u32> = (0..rank)
101            .map(|i| in_shape.dim(i).unwrap_static() as u32)
102            .collect();
103        let mut rev_mask = vec![false; rank];
104        for &a in axes {
105            if a < rank {
106                rev_mask[a] = true;
107            }
108        }
109        Thunk::Reverse {
110            src: node_offset(arena, node.inputs[0]),
111            dst: node_offset(arena, node.id),
112            dims,
113            rev_mask,
114            elem_bytes: in_shape.dtype().size_bytes() as u8,
115        }
116    }
117}
118
119#[allow(unused_variables)]
120pub(crate) fn compile_cast(
121    node: &rlx_ir::Node,
122    graph: &Graph,
123    arena: &crate::arena::Arena,
124    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
125    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
126    rng: rlx_ir::RngOptions,
127) -> Thunk {
128    let Op::Cast { to } = &node.op else {
129        unreachable!()
130    };
131    {
132        let in_node = graph.node(node.inputs[0]);
133        let in_dtype = in_node.shape.dtype();
134        let out_dtype = *to;
135        let len = node.shape.num_elements().unwrap();
136        let src = node_offset(arena, node.inputs[0]);
137        let dst = node_offset(arena, node.id);
138        if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::I64 {
139            Thunk::CastF32ToI64 {
140                src,
141                dst,
142                len: len as u32,
143            }
144        } else if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::F64 {
145            Thunk::CastF32ToF64 {
146                src,
147                dst,
148                len: len as u32,
149            }
150        } else if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::I32 {
151            Thunk::CastF32ToI32 {
152                src,
153                dst,
154                len: len as u32,
155            }
156        } else if in_dtype == rlx_ir::DType::I64 && out_dtype == rlx_ir::DType::F32 {
157            Thunk::CastI64ToF32 {
158                src,
159                dst,
160                len: len as u32,
161            }
162        } else if in_dtype == rlx_ir::DType::Bool && out_dtype == rlx_ir::DType::I32 {
163            Thunk::CastBoolToI32 {
164                src,
165                dst,
166                len: len as u32,
167            }
168        } else if in_dtype == rlx_ir::DType::Bool && out_dtype == rlx_ir::DType::F32 {
169            // Bool is 1 byte; the generic f32 Copy below would misread it as
170            // 4-byte f32. VITS sequence masks are `Cast(Less(...), f32)`.
171            Thunk::CastBoolToF32 {
172                src,
173                dst,
174                len: len as u32,
175            }
176        } else if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::Bool {
177            // f32 → 1-byte Bool. Output is native-width Bool (1 byte); the
178            // generic Copy would write 4 bytes. Used by logical And/Or.
179            Thunk::CastF32ToBool {
180                src,
181                dst,
182                len: len as u32,
183            }
184        } else if in_dtype == rlx_ir::DType::I32 && out_dtype == rlx_ir::DType::F32 {
185            Thunk::CastI32ToF32 {
186                src,
187                dst,
188                len: len as u32,
189            }
190        } else if in_dtype == rlx_ir::DType::I32 && out_dtype == rlx_ir::DType::I64 {
191            Thunk::CastI32ToI64 {
192                src,
193                dst,
194                len: len as u32,
195            }
196        } else if in_dtype == rlx_ir::DType::I32 && out_dtype == rlx_ir::DType::Bool {
197            Thunk::CastI32ToBool {
198                src,
199                dst,
200                len: len as u32,
201            }
202        } else if in_dtype == rlx_ir::DType::I64 && out_dtype == rlx_ir::DType::Bool {
203            Thunk::CastI64ToBool {
204                src,
205                dst,
206                len: len as u32,
207            }
208        } else if in_dtype == rlx_ir::DType::Bool && out_dtype == rlx_ir::DType::I64 {
209            Thunk::CastBoolToI64 {
210                src,
211                dst,
212                len: len as u32,
213            }
214        } else if in_dtype == out_dtype {
215            match out_dtype {
216                rlx_ir::DType::F64 => Thunk::CopyF64 {
217                    src,
218                    dst,
219                    len: len as u32,
220                },
221                rlx_ir::DType::I64 => Thunk::CopyI64 {
222                    src,
223                    dst,
224                    len: len as u32,
225                },
226                _ => Thunk::Copy {
227                    src,
228                    dst,
229                    len: len as u32,
230                },
231            }
232        } else {
233            Thunk::Copy {
234                src,
235                dst,
236                len: len as u32,
237            }
238        }
239    }
240}
241
242#[allow(unused_variables)]
243pub(crate) fn compile_expand(
244    node: &rlx_ir::Node,
245    graph: &Graph,
246    arena: &crate::arena::Arena,
247    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
248    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
249    rng: rlx_ir::RngOptions,
250) -> Thunk {
251    let Op::Expand { .. } = &node.op else {
252        unreachable!()
253    };
254    {
255        // Broadcast: build per-output-dim strides where any input dim
256        // of size 1 has stride 0 (read the same element repeatedly).
257        // Reuses the Thunk::Transpose runtime — N-D walk with strides
258        // is identical; only the strides differ.
259        let in_shape = &graph.node(node.inputs[0]).shape;
260        let out_shape = &node.shape;
261        let in_rank = in_shape.rank();
262        let out_rank = out_shape.rank();
263        // Implicit leading 1s if input has lower rank.
264        let pad = out_rank.saturating_sub(in_rank);
265        let in_dims: Vec<usize> = (0..out_rank)
266            .map(|i| {
267                if i < pad {
268                    1
269                } else {
270                    in_shape.dim(i - pad).unwrap_static()
271                }
272            })
273            .collect();
274        // Row-major input strides (over the padded shape).
275        let mut in_strides_full = vec![1usize; out_rank];
276        for d in (0..out_rank.saturating_sub(1)).rev() {
277            in_strides_full[d] = in_strides_full[d + 1] * in_dims[d + 1];
278        }
279        let out_dims: Vec<u32> = (0..out_rank)
280            .map(|i| out_shape.dim(i).unwrap_static() as u32)
281            .collect();
282        // Stride is 0 for broadcast dims (in_dim == 1 && out_dim > 1).
283        let in_strides: Vec<u32> = (0..out_rank)
284            .map(|i| {
285                if in_dims[i] == 1 && (out_dims[i] as usize) > 1 {
286                    0
287                } else {
288                    in_strides_full[i] as u32
289                }
290            })
291            .collect();
292        let in_total = in_dims.iter().product::<usize>() as u32;
293        let src = node_offset(arena, node.inputs[0]);
294        let dst = node_offset(arena, node.id);
295        let elem_bytes = node.shape.dtype().size_bytes() as u8;
296        match node.shape.dtype() {
297            rlx_ir::DType::F64 => Thunk::TransposeF64 {
298                src,
299                dst,
300                in_total,
301                out_dims,
302                in_strides,
303            },
304            _ => Thunk::Transpose {
305                src,
306                dst,
307                in_total,
308                out_dims,
309                in_strides,
310                elem_bytes,
311            },
312        }
313    }
314}
315
316#[allow(unused_variables)]
317pub(crate) fn compile_transpose(
318    node: &rlx_ir::Node,
319    graph: &Graph,
320    arena: &crate::arena::Arena,
321    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
322    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
323    rng: rlx_ir::RngOptions,
324) -> Thunk {
325    let Op::Transpose { perm } = &node.op else {
326        unreachable!()
327    };
328    {
329        // Pre-compute (out_dims, in_strides_for_each_out_dim) so the
330        // runtime loop is just an N-D index walk + scatter.
331        let in_shape = &graph.node(node.inputs[0]).shape;
332        let in_rank = in_shape.rank();
333        if perm.iter().any(|&p| p >= in_rank) {
334            Thunk::Nop
335        } else {
336            let in_dims: Vec<usize> = (0..in_rank)
337                .map(|i| in_shape.dim(i).unwrap_static())
338                .collect();
339            // Row-major input strides: stride[d] = product of dims[d+1..].
340            let mut in_strides_full = vec![1usize; in_rank];
341            for d in (0..in_rank.saturating_sub(1)).rev() {
342                in_strides_full[d] = in_strides_full[d + 1] * in_dims[d + 1];
343            }
344            let out_dims: Vec<u32> = perm.iter().map(|&p| in_dims[p] as u32).collect();
345            let in_strides: Vec<u32> = perm.iter().map(|&p| in_strides_full[p] as u32).collect();
346            let in_total = in_dims.iter().product::<usize>() as u32;
347            let src = node_offset(arena, node.inputs[0]);
348            let dst = node_offset(arena, node.id);
349            let elem_bytes = node.shape.dtype().size_bytes() as u8;
350            match node.shape.dtype() {
351                rlx_ir::DType::F64 => Thunk::TransposeF64 {
352                    src,
353                    dst,
354                    in_total,
355                    out_dims,
356                    in_strides,
357                },
358                _ => Thunk::Transpose {
359                    src,
360                    dst,
361                    in_total,
362                    out_dims,
363                    in_strides,
364                    elem_bytes,
365                },
366            }
367        }
368    }
369}
370
371#[allow(unused_variables)]
372pub(crate) fn compile_scatter_add(
373    node: &rlx_ir::Node,
374    graph: &Graph,
375    arena: &crate::arena::Arena,
376    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
377    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
378    rng: rlx_ir::RngOptions,
379) -> Thunk {
380    let Op::ScatterAdd = &node.op else {
381        unreachable!()
382    };
383    {
384        // updates: [num_updates, ...trailing], indices: [num_updates],
385        // output: [out_dim, ...trailing]
386        let upd_shape = &graph.node(node.inputs[0]).shape;
387        let out_shape = &node.shape;
388        let num_updates = upd_shape.dim(0).unwrap_static();
389        let out_dim = out_shape.dim(0).unwrap_static();
390        let trailing: usize = (1..out_shape.rank())
391            .map(|i| out_shape.dim(i).unwrap_static())
392            .product::<usize>()
393            .max(1);
394        Thunk::ScatterAdd {
395            updates: node_offset(arena, node.inputs[0]),
396            indices: node_offset(arena, node.inputs[1]),
397            dst: node_offset(arena, node.id),
398            num_updates: num_updates as u32,
399            out_dim: out_dim as u32,
400            trailing: trailing as u32,
401        }
402    }
403}
404
405#[allow(unused_variables)]
406pub(crate) fn compile_scatter_nd(
407    node: &rlx_ir::Node,
408    graph: &Graph,
409    arena: &crate::arena::Arena,
410    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
411    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
412    rng: rlx_ir::RngOptions,
413) -> Thunk {
414    let Op::ScatterNd { reduction } = &node.op else {
415        unreachable!()
416    };
417    let data_shape = &graph.node(node.inputs[0]).shape;
418    let indices_shape = &graph.node(node.inputs[1]).shape;
419    let updates_shape = &graph.node(node.inputs[2]).shape;
420    let data_len = data_shape.num_elements().unwrap_or(0);
421    let updates_len = updates_shape.num_elements().unwrap_or(0);
422    let indices_len = indices_shape.num_elements().unwrap_or(0);
423    let indices_i64 = u8::from(indices_shape.dtype() == rlx_ir::DType::I64);
424    Thunk::ScatterNd {
425        data: node_offset(arena, node.inputs[0]),
426        indices: node_offset(arena, node.inputs[1]),
427        updates: node_offset(arena, node.inputs[2]),
428        dst: node_offset(arena, node.id),
429        data_shape: (0..data_shape.rank())
430            .map(|i| data_shape.dim(i).unwrap_static() as u32)
431            .collect(),
432        indices_shape: (0..indices_shape.rank())
433            .map(|i| indices_shape.dim(i).unwrap_static() as u32)
434            .collect(),
435        data_len: data_len as u32,
436        updates_len: updates_len as u32,
437        indices_len: indices_len as u32,
438        indices_i64,
439        reduction: *reduction,
440    }
441}
442
443fn shape_u32s(shape: &rlx_ir::Shape) -> Vec<u32> {
444    (0..shape.rank())
445        .map(|i| shape.dim(i).unwrap_static() as u32)
446        .collect()
447}
448
449#[allow(unused_variables)]
450pub(crate) fn compile_scatter_elements(
451    node: &rlx_ir::Node,
452    graph: &Graph,
453    arena: &crate::arena::Arena,
454    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
455    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
456    rng: rlx_ir::RngOptions,
457) -> Thunk {
458    let Op::ScatterElements { axis, reduction } = &node.op else {
459        unreachable!()
460    };
461    let data_shape = &graph.node(node.inputs[0]).shape;
462    let indices_shape = &graph.node(node.inputs[1]).shape;
463    let updates_shape = &graph.node(node.inputs[2]).shape;
464    Thunk::ScatterElements {
465        data: node_offset(arena, node.inputs[0]),
466        indices: node_offset(arena, node.inputs[1]),
467        updates: node_offset(arena, node.inputs[2]),
468        dst: node_offset(arena, node.id),
469        data_shape: shape_u32s(data_shape),
470        data_len: data_shape.num_elements().unwrap_or(0) as u32,
471        updates_len: updates_shape.num_elements().unwrap_or(0) as u32,
472        indices_len: indices_shape.num_elements().unwrap_or(0) as u32,
473        indices_i64: u8::from(indices_shape.dtype() == rlx_ir::DType::I64),
474        axis: *axis,
475        reduction: *reduction,
476    }
477}
478
479#[allow(unused_variables)]
480pub(crate) fn compile_gather_nd(
481    node: &rlx_ir::Node,
482    graph: &Graph,
483    arena: &crate::arena::Arena,
484    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
485    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
486    rng: rlx_ir::RngOptions,
487) -> Thunk {
488    let Op::GatherNd { batch_dims } = &node.op else {
489        unreachable!()
490    };
491    let data_shape = &graph.node(node.inputs[0]).shape;
492    let indices_shape = &graph.node(node.inputs[1]).shape;
493    Thunk::GatherNd {
494        data: node_offset(arena, node.inputs[0]),
495        indices: node_offset(arena, node.inputs[1]),
496        dst: node_offset(arena, node.id),
497        data_shape: shape_u32s(data_shape),
498        indices_shape: shape_u32s(indices_shape),
499        data_len: data_shape.num_elements().unwrap_or(0) as u32,
500        indices_len: indices_shape.num_elements().unwrap_or(0) as u32,
501        out_len: node.shape.num_elements().unwrap_or(0) as u32,
502        indices_i64: u8::from(indices_shape.dtype() == rlx_ir::DType::I64),
503        batch_dims: *batch_dims,
504    }
505}
506
507#[allow(unused_variables)]
508pub(crate) fn compile_gather_elements(
509    node: &rlx_ir::Node,
510    graph: &Graph,
511    arena: &crate::arena::Arena,
512    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
513    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
514    rng: rlx_ir::RngOptions,
515) -> Thunk {
516    let Op::GatherElements { axis } = &node.op else {
517        unreachable!()
518    };
519    let data_shape = &graph.node(node.inputs[0]).shape;
520    let indices_shape = &graph.node(node.inputs[1]).shape;
521    Thunk::GatherElements {
522        data: node_offset(arena, node.inputs[0]),
523        indices: node_offset(arena, node.inputs[1]),
524        dst: node_offset(arena, node.id),
525        data_shape: shape_u32s(data_shape),
526        indices_shape: shape_u32s(indices_shape),
527        data_len: data_shape.num_elements().unwrap_or(0) as u32,
528        indices_len: indices_shape.num_elements().unwrap_or(0) as u32,
529        out_len: node.shape.num_elements().unwrap_or(0) as u32,
530        indices_i64: u8::from(indices_shape.dtype() == rlx_ir::DType::I64),
531        axis: *axis,
532    }
533}
534
535#[allow(unused_variables)]
536pub(crate) fn compile_compare(
537    node: &rlx_ir::Node,
538    graph: &Graph,
539    arena: &crate::arena::Arena,
540    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
541    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
542    rng: rlx_ir::RngOptions,
543) -> Thunk {
544    let Op::Compare(cmp) = &node.op else {
545        unreachable!()
546    };
547    {
548        let len = node.shape.num_elements().unwrap();
549        let lhs_n = graph.node(node.inputs[0]).shape.num_elements().unwrap();
550        let rhs_n = graph.node(node.inputs[1]).shape.num_elements().unwrap();
551        let lhs_scalar = lhs_n == 1 && len > 1;
552        let rhs_scalar = rhs_n == 1 && len > 1;
553        let in_dtype = graph.node(node.inputs[0]).shape.dtype();
554        let inputs_i64 = u8::from(in_dtype == rlx_ir::DType::I64);
555        Thunk::Compare {
556            lhs: node_offset(arena, node.inputs[0]),
557            rhs: node_offset(arena, node.inputs[1]),
558            dst: node_offset(arena, node.id),
559            len: len as u32,
560            op: *cmp,
561            inputs_i64,
562            inputs_elem_bytes: in_dtype.size_bytes() as u8,
563            dst_elem_bytes: node.shape.dtype().size_bytes() as u8,
564            lhs_scalar,
565            rhs_scalar,
566        }
567    }
568}
569
570#[allow(unused_variables)]
571pub(crate) fn compile_where(
572    node: &rlx_ir::Node,
573    graph: &Graph,
574    arena: &crate::arena::Arena,
575    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
576    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
577    rng: rlx_ir::RngOptions,
578) -> Thunk {
579    let Op::Where = &node.op else { unreachable!() };
580    {
581        let len = node.shape.num_elements().unwrap();
582        let cn = graph.node(node.inputs[0]).shape.num_elements().unwrap();
583        let tn = graph.node(node.inputs[1]).shape.num_elements().unwrap();
584        let fn_ = graph.node(node.inputs[2]).shape.num_elements().unwrap();
585        let cond_scalar = cn == 1 && len > 1;
586        let true_scalar = tn == 1 && len > 1;
587        let false_scalar = fn_ == 1 && len > 1;
588        let elem_bytes = node.shape.dtype().size_bytes() as u8;
589        let cond_elem_bytes = graph.node(node.inputs[0]).shape.dtype().size_bytes() as u8;
590        Thunk::Where {
591            cond: node_offset(arena, node.inputs[0]),
592            on_true: node_offset(arena, node.inputs[1]),
593            on_false: node_offset(arena, node.inputs[2]),
594            dst: node_offset(arena, node.id),
595            len: len as u32,
596            elem_bytes,
597            cond_elem_bytes,
598            cond_scalar,
599            true_scalar,
600            false_scalar,
601        }
602    }
603}
604
605#[allow(unused_variables)]
606pub(crate) fn compile_fma(
607    node: &rlx_ir::Node,
608    graph: &Graph,
609    arena: &crate::arena::Arena,
610    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
611    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
612    rng: rlx_ir::RngOptions,
613) -> Thunk {
614    let Op::Fma = &node.op else { unreachable!() };
615    {
616        let len = node.shape.num_elements().unwrap();
617        Thunk::Fma {
618            a: node_offset(arena, node.inputs[0]),
619            b: node_offset(arena, node.inputs[1]),
620            c: node_offset(arena, node.inputs[2]),
621            dst: node_offset(arena, node.id),
622            len: len as u32,
623            elem_bytes: node.shape.dtype().size_bytes() as u8,
624        }
625    }
626}
627
628#[allow(unused_variables)]
629pub(crate) fn compile_relu_backward(
630    node: &rlx_ir::Node,
631    graph: &Graph,
632    arena: &crate::arena::Arena,
633    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
634    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
635    rng: rlx_ir::RngOptions,
636) -> Thunk {
637    let Op::ReluBackward = &node.op else {
638        unreachable!()
639    };
640    {
641        let len: usize = (0..node.shape.rank())
642            .map(|i| node.shape.dim(i).unwrap_static())
643            .product();
644        let x = node_offset(arena, node.inputs[0]);
645        let dy = node_offset(arena, node.inputs[1]);
646        let dx = node_offset(arena, node.id);
647        match node.shape.dtype() {
648            rlx_ir::DType::F64 => Thunk::ReluBackwardF64 {
649                x,
650                dy,
651                dx,
652                len: len as u32,
653            },
654            _ => Thunk::ReluBackward {
655                x,
656                dy,
657                dx,
658                len: len as u32,
659            },
660        }
661    }
662}
663
664#[allow(unused_variables)]
665pub(crate) fn compile_activation_backward(
666    node: &rlx_ir::Node,
667    graph: &Graph,
668    arena: &crate::arena::Arena,
669    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
670    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
671    rng: rlx_ir::RngOptions,
672) -> Thunk {
673    let Op::ActivationBackward { kind } = &node.op else {
674        unreachable!()
675    };
676    {
677        let len: usize = (0..node.shape.rank())
678            .map(|i| node.shape.dim(i).unwrap_static())
679            .product();
680        let x = node_offset(arena, node.inputs[0]);
681        let dy = node_offset(arena, node.inputs[1]);
682        let dx = node_offset(arena, node.id);
683        match node.shape.dtype() {
684            rlx_ir::DType::F64 => Thunk::ActivationBackwardF64 {
685                x,
686                dy,
687                dx,
688                len: len as u32,
689                kind: *kind,
690            },
691            _ => Thunk::ActivationBackward {
692                x,
693                dy,
694                dx,
695                len: len as u32,
696                kind: *kind,
697            },
698        }
699    }
700}
701
702#[allow(unused_variables)]
703pub(crate) fn compile_gather_backward(
704    node: &rlx_ir::Node,
705    graph: &Graph,
706    arena: &crate::arena::Arena,
707    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
708    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
709    rng: rlx_ir::RngOptions,
710) -> Thunk {
711    let Op::GatherBackward { .. } = &node.op else {
712        unreachable!()
713    };
714    {
715        let dy_shape = &graph.node(node.inputs[0]).shape;
716        let idx_shape = &graph.node(node.inputs[1]).shape;
717        let out_shape = &node.shape;
718        let rank = out_shape.rank();
719        let axis = match &node.op {
720            Op::GatherBackward { axis } => *axis,
721            _ => 0,
722        };
723        let axis_u = if axis < 0 {
724            (rank as i32 + axis) as usize
725        } else {
726            axis as usize
727        };
728        let outer: usize = (0..axis_u)
729            .map(|i| dy_shape.dim(i).unwrap_static())
730            .product::<usize>()
731            .max(1);
732        let num_idx = idx_shape.dim(axis_u).unwrap_static();
733        let trailing: usize = (axis_u + 1..dy_shape.rank())
734            .map(|i| dy_shape.dim(i).unwrap_static())
735            .product::<usize>()
736            .max(1);
737        let axis_dim = out_shape.dim(axis_u).unwrap_static();
738        Thunk::GatherBackward {
739            dy: node_offset(arena, node.inputs[0]),
740            indices: node_offset(arena, node.inputs[1]),
741            dst: node_offset(arena, node.id),
742            outer: outer as u32,
743            axis_dim: axis_dim as u32,
744            num_idx: num_idx as u32,
745            trailing: trailing as u32,
746        }
747    }
748}
749
750#[allow(unused_variables)]
751pub(crate) fn compile_concat(
752    node: &rlx_ir::Node,
753    graph: &Graph,
754    arena: &crate::arena::Arena,
755    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
756    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
757    rng: rlx_ir::RngOptions,
758) -> Thunk {
759    let Op::Concat { axis } = &node.op else {
760        unreachable!()
761    };
762    {
763        // Compute outer/inner from the OUTPUT shape: all inputs share
764        // the same shape except along `axis`. The output's leading
765        // and trailing dims match.
766        let out_shape = &node.shape;
767        let rank = out_shape.rank();
768        let outer: usize = (0..*axis)
769            .map(|i| out_shape.dim(i).unwrap_static())
770            .product::<usize>()
771            .max(1);
772        let inner: usize = (*axis + 1..rank)
773            .map(|i| out_shape.dim(i).unwrap_static())
774            .product::<usize>()
775            .max(1);
776        let total_axis = out_shape.dim(*axis).unwrap_static();
777        let inputs: Vec<(usize, u32, u32)> = node
778            .inputs
779            .iter()
780            .map(|&in_id| {
781                let in_shape = &graph.node(in_id).shape;
782                let in_axis = concat_axis_extent(in_shape, *axis, rank);
783                let in_numel = in_shape.num_elements().unwrap_or(0) as u32;
784                (node_offset(arena, in_id), in_axis as u32, in_numel)
785            })
786            .collect();
787        let dst = node_offset(arena, node.id);
788        // The `Concat` thunk copies 4-byte (f32-sized) elements; `ConcatF64`
789        // copies 8-byte elements. Route by element SIZE, not just F64 — an I64
790        // tensor (8-byte) sent through the 4-byte path reads/writes half the
791        // bytes per element, so it copies only the first half of each input and
792        // leaves the tail zero (e.g. `Tile([9..0], 3)` on i64 rel-shift indices
793        // → `[9,8,7,6,5]×3 + zeros`). The 8-byte copy is a pure byte move, so
794        // reinterpreting i64 as f64 for the copy is bit-preserving.
795        match out_shape.dtype().size_bytes() {
796            8 => Thunk::ConcatF64 {
797                dst,
798                outer: outer as u32,
799                inner: inner as u32,
800                total_axis: total_axis as u32,
801                inputs,
802            },
803            _ => Thunk::Concat {
804                dst,
805                outer: outer as u32,
806                inner: inner as u32,
807                total_axis: total_axis as u32,
808                inputs,
809            },
810        }
811    }
812}
813
814#[allow(unused_variables)]
815pub(crate) fn compile_elementwise_region(
816    node: &rlx_ir::Node,
817    graph: &Graph,
818    arena: &crate::arena::Arena,
819    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
820    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
821    rng: rlx_ir::RngOptions,
822) -> Thunk {
823    let Op::ElementwiseRegion {
824        chain,
825        scalar_input_mask,
826        input_modulus,
827        prologue,
828        ..
829    } = &node.op
830    else {
831        unreachable!()
832    };
833    {
834        // The scalar interpreter handles plain chains; prologue (resize)
835        // regions are GPU-only and never reach here on the CPU path
836        // (the graphfused fusion options disable prologue/FK fusion).
837        if *prologue != rlx_ir::op::RegionPrologue::None {
838            Thunk::Nop
839        } else {
840            let input_offs: Vec<usize> = node
841                .inputs
842                .iter()
843                .map(|&id| node_offset(arena, id))
844                .collect();
845            Thunk::ElementwiseRegion {
846                dst: node_offset(arena, node.id),
847                len: node.shape.num_elements().unwrap_or(0) as u32,
848                input_offs,
849                chain: chain.clone(),
850                scalar_input_mask: *scalar_input_mask,
851                input_modulus: *input_modulus,
852            }
853        }
854    }
855}
856
857pub(crate) fn get_len(graph: &Graph, id: NodeId) -> usize {
858    graph.node(id).shape.num_elements().unwrap_or(0)
859}
860
861/// Static `usize` dims of a node's shape, or empty if any dim is dynamic.
862pub(crate) fn get_static_dims(graph: &Graph, id: NodeId) -> Vec<usize> {
863    let dims = graph.node(id).shape.dims();
864    let mut out = Vec::with_capacity(dims.len());
865    for d in dims {
866        if let Some(s) = match d {
867            rlx_ir::Dim::Static(s) => Some(*s),
868            _ => None,
869        } {
870            out.push(s);
871        } else {
872            return Vec::new();
873        }
874    }
875    out
876}
877
878/// Extent along `axis` for a concat input, treating leading implicit 1s when
879/// `input.rank() < output.rank()` (ONNX / numpy concat broadcast rules).
880pub(crate) fn concat_axis_extent(input: &rlx_ir::Shape, axis: usize, out_rank: usize) -> usize {
881    let in_rank = input.rank();
882    if axis >= out_rank {
883        return 1;
884    }
885    if axis < in_rank {
886        input.dim(axis).unwrap_static()
887    } else {
888        1
889    }
890}
891
892pub(crate) fn broadcast_src_index(src_idx: usize, in_len: usize) -> usize {
893    if in_len == 0 { 0 } else { src_idx % in_len }
894}
895
896/// Copy `inp` into row-strided `out` for a Concat input, with outer-dim
897/// broadcasting. The body is dtype-agnostic (only `Copy` + slice ops), so the
898/// f32/f64 variants are generated from one template rather than copy-pasted.
899macro_rules! concat_copy_rows {
900    ($name:ident, $t:ty) => {
901        pub(crate) fn $name(
902            out: &mut [$t],
903            inp: &[$t],
904            outer: usize,
905            copy_per_row: usize,
906            row_stride: usize,
907            dst_col_off: usize,
908            in_numel: usize,
909        ) {
910            let need = outer.saturating_mul(copy_per_row.max(1));
911            let broadcast_outer = in_numel < need;
912            let out_ptr = out.as_mut_ptr() as usize;
913            let inp_ptr = inp.as_ptr() as usize;
914            let inp_len = inp.len();
915            let run = |o0: usize, o1: usize| {
916                let out = unsafe { std::slice::from_raw_parts_mut(out_ptr as *mut $t, out.len()) };
917                let inp = unsafe { std::slice::from_raw_parts(inp_ptr as *const $t, inp_len) };
918                for o in o0..o1 {
919                    let dst_row_start = o * row_stride + dst_col_off;
920                    if broadcast_outer {
921                        if in_numel == 1 {
922                            if copy_per_row == 1 {
923                                out[dst_row_start] = inp[0];
924                            } else {
925                                out[dst_row_start..dst_row_start + copy_per_row].fill(inp[0]);
926                            }
927                        } else if copy_per_row <= inp.len() {
928                            out[dst_row_start..dst_row_start + copy_per_row]
929                                .copy_from_slice(&inp[..copy_per_row]);
930                        } else if !inp.is_empty() {
931                            out[dst_row_start..dst_row_start + copy_per_row].fill(inp[0]);
932                        }
933                    } else {
934                        let src_row_start = o * copy_per_row;
935                        out[dst_row_start..dst_row_start + copy_per_row]
936                            .copy_from_slice(&inp[src_row_start..src_row_start + copy_per_row]);
937                    }
938                }
939            };
940            // F5 DiT concatenates large mel/text streams — parallelize outer rows.
941            if outer >= 8
942                && copy_per_row >= 16
943                && crate::pool::num_threads() > 1
944                && crate::pool::should_parallelize(outer.saturating_mul(copy_per_row))
945            {
946                crate::pool::par_for(outer, 1, &|off, cnt| run(off, off + cnt));
947            } else {
948                run(0, outer);
949            }
950        }
951    };
952}
953
954concat_copy_rows!(concat_copy_rows_f32, f32);
955concat_copy_rows!(concat_copy_rows_f64, f64);
956
957/// NumPy-style broadcast strides for one operand into the flat output
958/// buffer. Returns a length-`out_dims.len()` `Vec<u32>` where entry
959/// `d` is `0` if the input is size-1 (broadcast) at output dim `d`
960/// (after left-padding with size-1 to match ranks), otherwise the
961/// natural row-major stride into the *input* buffer.
962///
963/// Caller iterates output flat index `i` → output coords (row-major)
964/// → input flat index = dot(coords, strides). The result is correct
965/// for any broadcast pattern (scalar, last-axis, middle-axis,
966/// bidirectional).
967/// True when `rhs_dims` describes a *trailing* broadcast of `out_dims`
968/// — i.e. every rhs dim either equals the corresponding output dim
969/// (counting from the right) or rhs is shorter (left-padded with 1s).
970/// Mid-shape singletons (e.g. rhs `[a, b, 1, d]` into out `[a, b, c, d]`
971/// where `c > 1`) are NOT trailing broadcasts and require the
972/// shape-aware `BinaryFull` slow path — `BiasAdd`'s linear bias-replicated
973/// kernel silently miscomputes them.
974pub(crate) fn is_trailing_bias_broadcast(
975    rhs_dims: &[rlx_ir::Dim],
976    out_dims: &[rlx_ir::Dim],
977) -> bool {
978    if rhs_dims.len() > out_dims.len() {
979        return false;
980    }
981    let off = out_dims.len() - rhs_dims.len();
982    for i in 0..rhs_dims.len() {
983        let r = match rhs_dims[i] {
984            rlx_ir::Dim::Static(n) => n,
985            _ => return false,
986        };
987        let o = match out_dims[off + i] {
988            rlx_ir::Dim::Static(n) => n,
989            _ => return false,
990        };
991        if r != o {
992            return false;
993        }
994    }
995    true
996}
997
998pub(crate) fn broadcast_strides(in_dims: &[usize], out_dims: &[usize]) -> Vec<u32> {
999    let r_out = out_dims.len();
1000    let r_in = in_dims.len();
1001    assert!(
1002        r_in <= r_out,
1003        "broadcast: input rank {r_in} > output rank {r_out}"
1004    );
1005    let pad = r_out - r_in;
1006    let mut strides = vec![0u32; r_out];
1007    let mut acc: usize = 1;
1008    for d in (0..r_out).rev() {
1009        let in_size = if d < pad { 1 } else { in_dims[d - pad] };
1010        if in_size == 1 {
1011            strides[d] = 0;
1012        } else {
1013            assert_eq!(
1014                in_size, out_dims[d],
1015                "broadcast: input dim {in_size} doesn't match output dim {} at axis {d}",
1016                out_dims[d]
1017            );
1018            strides[d] = acc as u32;
1019            acc *= in_size;
1020        }
1021    }
1022    strides
1023}
1024
1025#[inline(always)]
1026pub(crate) fn exec_elementwise_region(t: &Thunk, base: *mut u8) {
1027    let Thunk::ElementwiseRegion {
1028        dst,
1029        len,
1030        input_offs,
1031        chain,
1032        scalar_input_mask,
1033        input_modulus,
1034    } = t
1035    else {
1036        unreachable!()
1037    };
1038    {
1039        let len = *len as usize;
1040        if !chain.is_empty() && len > 0 {
1041            let base_addr = base as usize;
1042            let dst = *dst;
1043            let scalar_mask = *scalar_input_mask;
1044            // Each output element is independent → fan over the range.
1045            let eval = |gid: usize| {
1046                let v = region_eval_elem(
1047                    gid,
1048                    base_addr as *const u8,
1049                    input_offs,
1050                    chain,
1051                    scalar_mask,
1052                    input_modulus,
1053                );
1054                unsafe {
1055                    *((base_addr as *mut u8).add(dst) as *mut f32).add(gid) = v;
1056                }
1057            };
1058            if fast_conv_enabled() && crate::pool::should_parallelize(len) {
1059                crate::pool::par_for(len, crate::pool::chunk_floor(len), &|off, cnt| {
1060                    for gid in off..off + cnt {
1061                        eval(gid);
1062                    }
1063                });
1064            } else {
1065                for gid in 0..len {
1066                    eval(gid);
1067                }
1068            }
1069        }
1070    }
1071}
1072
1073#[inline(always)]
1074pub(crate) fn exec_transpose_f64(t: &Thunk, base: *mut u8) {
1075    let Thunk::TransposeF64 {
1076        src,
1077        dst,
1078        in_total,
1079        out_dims,
1080        in_strides,
1081    } = t
1082    else {
1083        unreachable!()
1084    };
1085    unsafe {
1086        let inp = sl_f64(*src, base, *in_total as usize);
1087        let out_total: usize = out_dims.iter().map(|d| *d as usize).product();
1088        let out = sl_mut_f64(*dst, base, out_total);
1089        transpose_walk_f64(inp, out, out_dims, in_strides);
1090    }
1091}
1092
1093#[inline(always)]
1094pub(crate) fn exec_activation_f64(t: &Thunk, base: *mut u8) {
1095    let Thunk::ActivationF64 {
1096        src,
1097        dst,
1098        len,
1099        kind,
1100    } = t
1101    else {
1102        unreachable!()
1103    };
1104    {
1105        let len = *len as usize;
1106        unsafe {
1107            let inp = sl_f64(*src, base, len);
1108            let out = sl_mut_f64(*dst, base, len);
1109            apply_activation_f64(inp, out, *kind);
1110        }
1111    }
1112}
1113
1114#[inline(always)]
1115pub(crate) fn exec_binary_full_f64(t: &Thunk, base: *mut u8) {
1116    let Thunk::BinaryFullF64 {
1117        lhs,
1118        rhs,
1119        dst,
1120        len,
1121        lhs_len,
1122        rhs_len,
1123        op,
1124        out_dims_bcast,
1125        bcast_lhs_strides,
1126        bcast_rhs_strides,
1127    } = t
1128    else {
1129        unreachable!()
1130    };
1131    {
1132        let len = *len as usize;
1133        let lhs_len = *lhs_len as usize;
1134        let rhs_len = *rhs_len as usize;
1135        unsafe {
1136            let l = sl_f64(*lhs, base, lhs_len);
1137            let r = sl_f64(*rhs, base, rhs_len);
1138            let d = sl_mut_f64(*dst, base, len);
1139            if lhs_len == len && rhs_len == len {
1140                for i in 0..len {
1141                    d[i] = binary_op_f64(*op, l[i], r[i]);
1142                }
1143            } else if !out_dims_bcast.is_empty() {
1144                // Shape-aware broadcast path: correct for
1145                // arbitrary NumPy-style broadcasts including
1146                // bidirectional `[N,1] op [1,S]`.
1147                let rank = out_dims_bcast.len();
1148                let mut coords = vec![0u32; rank];
1149                for i in 0..len {
1150                    let mut rem = i;
1151                    for ax in (0..rank).rev() {
1152                        let sz = out_dims_bcast[ax] as usize;
1153                        coords[ax] = (rem % sz) as u32;
1154                        rem /= sz;
1155                    }
1156                    let mut li: usize = 0;
1157                    let mut ri: usize = 0;
1158                    for ax in 0..rank {
1159                        li += coords[ax] as usize * bcast_lhs_strides[ax] as usize;
1160                        ri += coords[ax] as usize * bcast_rhs_strides[ax] as usize;
1161                    }
1162                    d[i] = binary_op_f64(*op, l[li], r[ri]);
1163                }
1164            } else {
1165                // Fallback: legacy modulo path (preserved for
1166                // dynamic-shape graphs where strides can't be
1167                // precomputed). Only correct for scalar /
1168                // last-axis broadcast.
1169                for i in 0..len {
1170                    d[i] = binary_op_f64(*op, l[i % lhs_len], r[i % rhs_len]);
1171                }
1172            }
1173        }
1174    }
1175}
1176
1177#[inline(always)]
1178pub(crate) fn exec_binary_full_c64(t: &Thunk, base: *mut u8) {
1179    let Thunk::BinaryFullC64 {
1180        lhs,
1181        rhs,
1182        dst,
1183        len,
1184        lhs_len,
1185        rhs_len,
1186        op,
1187        out_dims_bcast,
1188        bcast_lhs_strides,
1189        bcast_rhs_strides,
1190    } = t
1191    else {
1192        unreachable!()
1193    };
1194    {
1195        // Complex element layout: [re_0, im_0, re_1, im_1, ...]
1196        // Underlying f32 buffer length is 2·N (N = complex
1197        // element count). All offsets are byte offsets; the
1198        // `sl` helper reads as f32 starting at the byte
1199        // offset, so f32-length = 2·complex-len.
1200        let n_out = *len as usize;
1201        let n_l = *lhs_len as usize;
1202        let n_r = *rhs_len as usize;
1203        unsafe {
1204            let l = sl(*lhs, base, 2 * n_l);
1205            let r = sl(*rhs, base, 2 * n_r);
1206            let d = sl_mut(*dst, base, 2 * n_out);
1207            let do_c64 = |a_re: f32, a_im: f32, b_re: f32, b_im: f32| -> (f32, f32) {
1208                match op {
1209                    BinaryOp::Add => (a_re + b_re, a_im + b_im),
1210                    BinaryOp::Sub => (a_re - b_re, a_im - b_im),
1211                    BinaryOp::Mul => (a_re * b_re - a_im * b_im, a_re * b_im + a_im * b_re),
1212                    BinaryOp::Div => {
1213                        let denom = b_re * b_re + b_im * b_im;
1214                        (
1215                            (a_re * b_re + a_im * b_im) / denom,
1216                            (a_im * b_re - a_re * b_im) / denom,
1217                        )
1218                    }
1219                    BinaryOp::Max | BinaryOp::Min | BinaryOp::Pow => {
1220                        unreachable!("C64 max/min/pow rejected at lowering")
1221                    }
1222                }
1223            };
1224            if n_l == n_out && n_r == n_out {
1225                for i in 0..n_out {
1226                    let (re, im) = do_c64(l[2 * i], l[2 * i + 1], r[2 * i], r[2 * i + 1]);
1227                    d[2 * i] = re;
1228                    d[2 * i + 1] = im;
1229                }
1230            } else if !out_dims_bcast.is_empty() {
1231                // Strided complex broadcast: strides are in
1232                // *complex element* units; multiply by 2 when
1233                // indexing into the f32 buffer.
1234                let rank = out_dims_bcast.len();
1235                let mut coords = vec![0u32; rank];
1236                for i in 0..n_out {
1237                    let mut rem = i;
1238                    for ax in (0..rank).rev() {
1239                        let sz = out_dims_bcast[ax] as usize;
1240                        coords[ax] = (rem % sz) as u32;
1241                        rem /= sz;
1242                    }
1243                    let mut li: usize = 0;
1244                    let mut ri: usize = 0;
1245                    for ax in 0..rank {
1246                        li += coords[ax] as usize * bcast_lhs_strides[ax] as usize;
1247                        ri += coords[ax] as usize * bcast_rhs_strides[ax] as usize;
1248                    }
1249                    let (re, im) = do_c64(l[2 * li], l[2 * li + 1], r[2 * ri], r[2 * ri + 1]);
1250                    d[2 * i] = re;
1251                    d[2 * i + 1] = im;
1252                }
1253            } else {
1254                // Modulo fallback (scalar / last-axis broadcast).
1255                for i in 0..n_out {
1256                    let li = if n_l == 1 { 0 } else { i % n_l };
1257                    let ri = if n_r == 1 { 0 } else { i % n_r };
1258                    let (re, im) = do_c64(l[2 * li], l[2 * li + 1], r[2 * ri], r[2 * ri + 1]);
1259                    d[2 * i] = re;
1260                    d[2 * i + 1] = im;
1261                }
1262            }
1263        }
1264    }
1265}
1266
1267#[inline(always)]
1268pub(crate) fn exec_activation_c64(t: &Thunk, base: *mut u8) {
1269    let Thunk::ActivationC64 {
1270        src,
1271        dst,
1272        len,
1273        kind,
1274    } = t
1275    else {
1276        unreachable!()
1277    };
1278    {
1279        let n = *len as usize;
1280        unsafe {
1281            let s = sl(*src, base, 2 * n);
1282            let d = sl_mut(*dst, base, 2 * n);
1283            for i in 0..n {
1284                let a = s[2 * i];
1285                let b = s[2 * i + 1];
1286                let (re, im) = match kind {
1287                    Activation::Neg => (-a, -b),
1288                    Activation::Exp => {
1289                        // exp(a + bi) = e^a · (cos b + i·sin b)
1290                        let ea = a.exp();
1291                        (ea * b.cos(), ea * b.sin())
1292                    }
1293                    Activation::Log => {
1294                        // log(z) = log|z| + i·arg(z), principal branch
1295                        let r = (a * a + b * b).sqrt();
1296                        (r.ln(), b.atan2(a))
1297                    }
1298                    Activation::Sqrt => {
1299                        // sqrt(a+bi) = sqrt((|z|+a)/2) + sign(b)·i·sqrt((|z|-a)/2)
1300                        // Principal branch; for b == 0 and a < 0 returns +i·sqrt(|a|).
1301                        let r = (a * a + b * b).sqrt();
1302                        let re = ((r + a) * 0.5).max(0.0).sqrt();
1303                        let im_mag = ((r - a) * 0.5).max(0.0).sqrt();
1304                        let im = if b >= 0.0 { im_mag } else { -im_mag };
1305                        (re, im)
1306                    }
1307                    _ => unreachable!("non-C64 activation kind survived lowering"),
1308                };
1309                d[2 * i] = re;
1310                d[2 * i + 1] = im;
1311            }
1312        }
1313    }
1314}
1315
1316#[inline(always)]
1317pub(crate) fn exec_gather(t: &Thunk, base: *mut u8) {
1318    let Thunk::Gather {
1319        table,
1320        table_len,
1321        idx,
1322        dst,
1323        num_idx,
1324        trailing,
1325        idx_i64,
1326        table_bytes,
1327    } = t
1328    else {
1329        unreachable!()
1330    };
1331    {
1332        let (ni, tr) = (*num_idx as usize, *trailing as usize);
1333        let rows = *table_len as usize / tr.max(1);
1334        unsafe {
1335            if *table_bytes == 8 {
1336                let tab = sl_i64(*table, base, *table_len as usize);
1337                let out = sl_mut_i64(*dst, base, ni * tr);
1338                if *idx_i64 != 0 {
1339                    let ids = sl_i64(*idx, base, ni);
1340                    for i in 0..ni {
1341                        let row = ids[i].max(0) as usize;
1342                        if row < rows {
1343                            out[i * tr..(i + 1) * tr]
1344                                .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
1345                        }
1346                    }
1347                } else {
1348                    let ids = sl(*idx, base, ni);
1349                    for i in 0..ni {
1350                        let row = ids[i] as usize;
1351                        if row < rows {
1352                            out[i * tr..(i + 1) * tr]
1353                                .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
1354                        }
1355                    }
1356                }
1357            } else {
1358                let tab = sl(*table, base, *table_len as usize);
1359                let out = sl_mut(*dst, base, ni * tr);
1360                if *idx_i64 != 0 {
1361                    let ids = sl_i64(*idx, base, ni);
1362                    for i in 0..ni {
1363                        let row = ids[i].max(0) as usize;
1364                        if row < rows {
1365                            out[i * tr..(i + 1) * tr]
1366                                .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
1367                        }
1368                    }
1369                } else {
1370                    let ids = sl(*idx, base, ni);
1371                    for i in 0..ni {
1372                        let row = ids[i] as usize;
1373                        if row < rows {
1374                            out[i * tr..(i + 1) * tr]
1375                                .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
1376                        }
1377                    }
1378                }
1379            }
1380        }
1381    }
1382}
1383
1384#[inline(always)]
1385pub(crate) fn exec_activation_in_place(t: &Thunk, base: *mut u8) {
1386    let Thunk::ActivationInPlace { data, len, act } = t else {
1387        unreachable!()
1388    };
1389    {
1390        let len = *len as usize;
1391        unsafe {
1392            let d = sl_mut(*data, base, len);
1393            // Serial scalar transcendentals were a per-op cost for models heavy in
1394            // tanh/sigmoid/exp (LSTM/GRU gates, vocoders). Apply elementwise in
1395            // parallel for large tensors (bit-exact — elements independent), mirroring
1396            // the already-parallel Gelu/Silu kernels. `should_parallelize` guards the
1397            // small case so tiny activations stay serial (no dispatch overhead).
1398            let par = crate::pool::should_parallelize(len);
1399            macro_rules! apply {
1400                ($f:expr) => {{
1401                    let f = $f;
1402                    if par {
1403                        use rayon::prelude::*;
1404                        d.par_iter_mut().for_each(|v| *v = f(*v));
1405                    } else {
1406                        for v in d.iter_mut() {
1407                            *v = f(*v);
1408                        }
1409                    }
1410                }};
1411            }
1412            match act {
1413                Activation::Gelu => crate::kernels::par_gelu_inplace(d),
1414                Activation::GeluApprox => crate::kernels::par_gelu_approx_inplace(d),
1415                Activation::Silu => crate::kernels::par_silu_inplace(d),
1416                Activation::Relu => apply!(|x: f32| x.max(0.0)),
1417                Activation::Sigmoid => apply!(|x: f32| 1.0 / (1.0 + (-x).exp())),
1418                Activation::Tanh => apply!(|x: f32| x.tanh()),
1419                Activation::Exp => apply!(|x: f32| x.exp()),
1420                Activation::Log => apply!(|x: f32| x.ln()),
1421                Activation::Sqrt => apply!(|x: f32| x.sqrt()),
1422                Activation::Rsqrt => apply!(|x: f32| 1.0 / x.sqrt()),
1423                Activation::Neg => apply!(|x: f32| -x),
1424                Activation::Abs => apply!(|x: f32| x.abs()),
1425                Activation::Round => apply!(|x: f32| x.round()),
1426                Activation::Sin => apply!(|x: f32| x.sin()),
1427                Activation::Cos => apply!(|x: f32| x.cos()),
1428                Activation::Tan => apply!(|x: f32| x.tan()),
1429                Activation::Atan => apply!(|x: f32| x.atan()),
1430            }
1431        }
1432    }
1433}
1434
1435#[inline(always)]
1436pub(crate) fn exec_concat(t: &Thunk, base: *mut u8) {
1437    let Thunk::Concat {
1438        dst,
1439        outer,
1440        inner,
1441        total_axis,
1442        inputs,
1443    } = t
1444    else {
1445        unreachable!()
1446    };
1447    {
1448        let outer = *outer as usize;
1449        let inner = *inner as usize;
1450        let total_axis = *total_axis as usize;
1451        let row_stride = total_axis * inner;
1452        let out_total = outer * row_stride;
1453        unsafe {
1454            let out = sl_mut(*dst, base, out_total);
1455            let mut cum: usize = 0;
1456            for (src_off, in_axis, in_numel) in inputs {
1457                let in_axis = *in_axis as usize;
1458                let copy_per_row = in_axis * inner;
1459                let dst_col_off = cum * inner;
1460                let inp = sl(*src_off, base, (*in_numel as usize).max(1));
1461                concat_copy_rows_f32(
1462                    out,
1463                    inp,
1464                    outer,
1465                    copy_per_row,
1466                    row_stride,
1467                    dst_col_off,
1468                    *in_numel as usize,
1469                );
1470                cum += in_axis;
1471            }
1472        }
1473    }
1474}
1475
1476#[inline(always)]
1477pub(crate) fn exec_concat_f64(t: &Thunk, base: *mut u8) {
1478    let Thunk::ConcatF64 {
1479        dst,
1480        outer,
1481        inner,
1482        total_axis,
1483        inputs,
1484    } = t
1485    else {
1486        unreachable!()
1487    };
1488    {
1489        let outer = *outer as usize;
1490        let inner = *inner as usize;
1491        let total_axis = *total_axis as usize;
1492        let row_stride = total_axis * inner;
1493        let out_total = outer * row_stride;
1494        unsafe {
1495            let out = sl_mut_f64(*dst, base, out_total);
1496            let mut cum: usize = 0;
1497            for (src_off, in_axis, in_numel) in inputs {
1498                let in_axis = *in_axis as usize;
1499                let copy_per_row = in_axis * inner;
1500                let dst_col_off = cum * inner;
1501                let inp = sl_f64(*src_off, base, (*in_numel as usize).max(1));
1502                concat_copy_rows_f64(
1503                    out,
1504                    inp,
1505                    outer,
1506                    copy_per_row,
1507                    row_stride,
1508                    dst_col_off,
1509                    *in_numel as usize,
1510                );
1511                cum += in_axis;
1512            }
1513        }
1514    }
1515}
1516
1517#[inline(always)]
1518pub(crate) fn exec_scatter_add(t: &Thunk, base: *mut u8) {
1519    let Thunk::ScatterAdd {
1520        updates,
1521        indices,
1522        dst,
1523        num_updates,
1524        out_dim,
1525        trailing,
1526    } = t
1527    else {
1528        unreachable!()
1529    };
1530    {
1531        let num_updates = *num_updates as usize;
1532        let out_dim = *out_dim as usize;
1533        let trailing = *trailing as usize;
1534        unsafe {
1535            let upd = sl(*updates, base, num_updates * trailing);
1536            let ids = sl(*indices, base, num_updates);
1537            let out = sl_mut(*dst, base, out_dim * trailing);
1538            // Zero the output first — semantics are accumulate-into-zeros.
1539            for v in out.iter_mut() {
1540                *v = 0.0;
1541            }
1542            for i in 0..num_updates {
1543                let row = ids[i] as usize;
1544                debug_assert!(row < out_dim, "ScatterAdd index out of range");
1545                let src_off = i * trailing;
1546                let dst_off = row * trailing;
1547                for j in 0..trailing {
1548                    out[dst_off + j] += upd[src_off + j];
1549                }
1550            }
1551        }
1552    }
1553}
1554
1555#[inline(always)]
1556pub(crate) fn exec_scatter_nd(t: &Thunk, base: *mut u8) {
1557    let Thunk::ScatterNd {
1558        data,
1559        indices,
1560        updates,
1561        dst,
1562        data_shape,
1563        indices_shape,
1564        data_len,
1565        updates_len,
1566        indices_len,
1567        indices_i64,
1568        reduction,
1569    } = t
1570    else {
1571        unreachable!()
1572    };
1573    let data_shape: Vec<usize> = data_shape.iter().map(|&d| d as usize).collect();
1574    let indices_shape: Vec<usize> = indices_shape.iter().map(|&d| d as usize).collect();
1575    let data_len = *data_len as usize;
1576    let updates_len = *updates_len as usize;
1577    let indices_len = *indices_len as usize;
1578    unsafe {
1579        let data_s = sl(*data, base, data_len);
1580        let updates_s = sl(*updates, base, updates_len);
1581        let out = sl_mut(*dst, base, data_len);
1582        let idx_i64: Vec<i64> = if *indices_i64 != 0 {
1583            let ptr = base.add(*indices) as *const i64;
1584            std::slice::from_raw_parts(ptr, indices_len).to_vec()
1585        } else {
1586            sl(*indices, base, indices_len)
1587                .iter()
1588                .map(|&x| x as i64)
1589                .collect()
1590        };
1591        let (offsets, slice) =
1592            crate::onnx_indexing::scatter_nd_dst_offsets(&data_shape, &idx_i64, &indices_shape);
1593        crate::onnx_indexing::scatter_nd_into_f32(
1594            data_s, updates_s, out, &offsets, slice, *reduction,
1595        );
1596    }
1597}
1598
1599fn load_indices_i64(base: *mut u8, off: usize, len: usize, as_i64: u8) -> Vec<i64> {
1600    unsafe {
1601        if as_i64 != 0 {
1602            let ptr = base.add(off) as *const i64;
1603            std::slice::from_raw_parts(ptr, len).to_vec()
1604        } else {
1605            sl(off, base, len).iter().map(|&x| x as i64).collect()
1606        }
1607    }
1608}
1609
1610#[inline(always)]
1611pub(crate) fn exec_scatter_elements(t: &Thunk, base: *mut u8) {
1612    let Thunk::ScatterElements {
1613        data,
1614        indices,
1615        updates,
1616        dst,
1617        data_shape,
1618        data_len,
1619        updates_len,
1620        indices_len,
1621        indices_i64,
1622        axis,
1623        reduction,
1624    } = t
1625    else {
1626        unreachable!()
1627    };
1628    let data_shape: Vec<usize> = data_shape.iter().map(|&d| d as usize).collect();
1629    unsafe {
1630        let data_s = sl(*data, base, *data_len as usize);
1631        let updates_s = sl(*updates, base, *updates_len as usize);
1632        let out = sl_mut(*dst, base, *data_len as usize);
1633        let idx = load_indices_i64(base, *indices, *indices_len as usize, *indices_i64);
1634        crate::onnx_indexing::scatter_elements_f32(
1635            data_s,
1636            updates_s,
1637            &idx,
1638            out,
1639            &data_shape,
1640            *axis,
1641            *reduction,
1642        );
1643    }
1644}
1645
1646#[inline(always)]
1647pub(crate) fn exec_gather_nd(t: &Thunk, base: *mut u8) {
1648    let Thunk::GatherNd {
1649        data,
1650        indices,
1651        dst,
1652        data_shape,
1653        indices_shape,
1654        data_len,
1655        indices_len,
1656        out_len,
1657        indices_i64,
1658        batch_dims,
1659    } = t
1660    else {
1661        unreachable!()
1662    };
1663    let data_shape: Vec<usize> = data_shape.iter().map(|&d| d as usize).collect();
1664    let indices_shape: Vec<usize> = indices_shape.iter().map(|&d| d as usize).collect();
1665    unsafe {
1666        let data_s = sl(*data, base, *data_len as usize);
1667        let out = sl_mut(*dst, base, *out_len as usize);
1668        let idx = load_indices_i64(base, *indices, *indices_len as usize, *indices_i64);
1669        let (offsets, slice) = crate::onnx_indexing::gather_nd_src_offsets(
1670            &data_shape,
1671            &idx,
1672            &indices_shape,
1673            (*batch_dims).max(0) as usize,
1674        );
1675        for (t, &off) in offsets.iter().enumerate() {
1676            let d0 = t * slice;
1677            for j in 0..slice {
1678                if let (Some(&s), Some(d)) = (data_s.get(off + j), out.get_mut(d0 + j)) {
1679                    *d = s;
1680                }
1681            }
1682        }
1683    }
1684}
1685
1686#[inline(always)]
1687pub(crate) fn exec_gather_elements(t: &Thunk, base: *mut u8) {
1688    let Thunk::GatherElements {
1689        data,
1690        indices,
1691        dst,
1692        data_shape,
1693        indices_shape,
1694        data_len,
1695        indices_len,
1696        out_len,
1697        indices_i64,
1698        axis,
1699    } = t
1700    else {
1701        unreachable!()
1702    };
1703    let data_shape: Vec<usize> = data_shape.iter().map(|&d| d as usize).collect();
1704    let indices_shape: Vec<usize> = indices_shape.iter().map(|&d| d as usize).collect();
1705    unsafe {
1706        let data_s = sl(*data, base, *data_len as usize);
1707        let out = sl_mut(*dst, base, *out_len as usize);
1708        let idx = load_indices_i64(base, *indices, *indices_len as usize, *indices_i64);
1709        crate::onnx_indexing::gather_elements_f32(
1710            data_s,
1711            &idx,
1712            out,
1713            &data_shape,
1714            &indices_shape,
1715            *axis,
1716        );
1717    }
1718}
1719
1720#[inline(always)]
1721pub(crate) fn exec_relu_backward(t: &Thunk, base: *mut u8) {
1722    let Thunk::ReluBackward { x, dy, dx, len } = t else {
1723        unreachable!()
1724    };
1725    {
1726        let len = *len as usize;
1727        unsafe {
1728            let xs = sl(*x, base, len);
1729            let dys = sl(*dy, base, len);
1730            let out = sl_mut(*dx, base, len);
1731            if fast_conv_enabled() && crate::pool::should_parallelize(len) {
1732                let oa = out.as_mut_ptr() as usize;
1733                crate::pool::par_for(len, crate::pool::chunk_floor(len), &|off, cnt| {
1734                    for i in off..off + cnt {
1735                        *((oa as *mut f32).add(i)) = if xs[i] > 0.0 { dys[i] } else { 0.0 };
1736                    }
1737                });
1738            } else {
1739                for i in 0..len {
1740                    out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
1741                }
1742            }
1743        }
1744    }
1745}
1746
1747#[inline(always)]
1748pub(crate) fn exec_relu_backward_f64(t: &Thunk, base: *mut u8) {
1749    let Thunk::ReluBackwardF64 { x, dy, dx, len } = t else {
1750        unreachable!()
1751    };
1752    {
1753        let len = *len as usize;
1754        unsafe {
1755            let xs = sl_f64(*x, base, len);
1756            let dys = sl_f64(*dy, base, len);
1757            let out = sl_mut_f64(*dx, base, len);
1758            for i in 0..len {
1759                out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
1760            }
1761        }
1762    }
1763}
1764
1765#[inline(always)]
1766pub(crate) fn exec_activation_backward(t: &Thunk, base: *mut u8) {
1767    let Thunk::ActivationBackward {
1768        x,
1769        dy,
1770        dx,
1771        len,
1772        kind,
1773    } = t
1774    else {
1775        unreachable!()
1776    };
1777    {
1778        let len = *len as usize;
1779        unsafe {
1780            let xs = sl(*x, base, len);
1781            let dys = sl(*dy, base, len);
1782            let out = sl_mut(*dx, base, len);
1783            activation_backward_kernel(*kind, xs, dys, out);
1784        }
1785    }
1786}
1787
1788#[inline(always)]
1789pub(crate) fn exec_activation_backward_f64(t: &Thunk, base: *mut u8) {
1790    let Thunk::ActivationBackwardF64 {
1791        x,
1792        dy,
1793        dx,
1794        len,
1795        kind,
1796    } = t
1797    else {
1798        unreachable!()
1799    };
1800    {
1801        let len = *len as usize;
1802        unsafe {
1803            let xs = sl_f64(*x, base, len);
1804            let dys = sl_f64(*dy, base, len);
1805            let out = sl_mut_f64(*dx, base, len);
1806            activation_backward_kernel_f64(*kind, xs, dys, out);
1807        }
1808    }
1809}
1810
1811#[inline(always)]
1812pub(crate) fn exec_gather_backward(t: &Thunk, base: *mut u8) {
1813    let Thunk::GatherBackward {
1814        dy,
1815        indices,
1816        dst,
1817        outer,
1818        axis_dim,
1819        num_idx,
1820        trailing,
1821    } = t
1822    else {
1823        unreachable!()
1824    };
1825    {
1826        let (outer, axis_dim, num_idx, trailing) = (
1827            *outer as usize,
1828            *axis_dim as usize,
1829            *num_idx as usize,
1830            *trailing as usize,
1831        );
1832        unsafe {
1833            let dys = sl(*dy, base, outer * num_idx * trailing);
1834            let ids = sl(*indices, base, num_idx);
1835            let out = sl_mut(*dst, base, outer * axis_dim * trailing);
1836            for v in out.iter_mut() {
1837                *v = 0.0;
1838            }
1839            crate::training_bwd::gather_axis_backward(
1840                dys, ids, out, outer, axis_dim, num_idx, trailing,
1841            );
1842        }
1843    }
1844}
1845
1846#[inline(always)]
1847pub(crate) fn exec_gather_axis(t: &Thunk, base: *mut u8) {
1848    let Thunk::GatherAxis {
1849        table,
1850        idx,
1851        dst,
1852        outer,
1853        axis_dim,
1854        num_idx,
1855        trailing,
1856        idx_i64,
1857        table_bytes,
1858    } = t
1859    else {
1860        unreachable!()
1861    };
1862    {
1863        let outer = *outer as usize;
1864        let axis_dim = *axis_dim as usize;
1865        let num_idx = *num_idx as usize;
1866        let trailing = *trailing as usize;
1867        unsafe {
1868            if *table_bytes == 8 {
1869                let tab = sl_i64(*table, base, outer * axis_dim * trailing);
1870                let out = sl_mut_i64(*dst, base, outer * num_idx * trailing);
1871                for o in 0..outer {
1872                    let tab_outer = o * axis_dim * trailing;
1873                    let out_outer = o * num_idx * trailing;
1874                    if *idx_i64 != 0 {
1875                        let ids = sl_i64(*idx, base, num_idx);
1876                        for k in 0..num_idx {
1877                            let row = ids[k].max(0) as usize;
1878                            if row < axis_dim {
1879                                let tab_row = tab_outer + row * trailing;
1880                                let out_row = out_outer + k * trailing;
1881                                out[out_row..out_row + trailing]
1882                                    .copy_from_slice(&tab[tab_row..tab_row + trailing]);
1883                            }
1884                        }
1885                    } else {
1886                        let ids = sl(*idx, base, num_idx);
1887                        for k in 0..num_idx {
1888                            let row = ids[k] as usize;
1889                            if row < axis_dim {
1890                                let tab_row = tab_outer + row * trailing;
1891                                let out_row = out_outer + k * trailing;
1892                                out[out_row..out_row + trailing]
1893                                    .copy_from_slice(&tab[tab_row..tab_row + trailing]);
1894                            }
1895                        }
1896                    }
1897                }
1898            } else {
1899                let tab = sl(*table, base, outer * axis_dim * trailing);
1900                let out = sl_mut(*dst, base, outer * num_idx * trailing);
1901                for o in 0..outer {
1902                    let tab_outer = o * axis_dim * trailing;
1903                    let out_outer = o * num_idx * trailing;
1904                    if *idx_i64 != 0 {
1905                        let ids = sl_i64(*idx, base, num_idx);
1906                        for k in 0..num_idx {
1907                            let row = ids[k].max(0) as usize;
1908                            if row < axis_dim {
1909                                let tab_row = tab_outer + row * trailing;
1910                                let out_row = out_outer + k * trailing;
1911                                out[out_row..out_row + trailing]
1912                                    .copy_from_slice(&tab[tab_row..tab_row + trailing]);
1913                            }
1914                        }
1915                    } else {
1916                        let ids = sl(*idx, base, num_idx);
1917                        for k in 0..num_idx {
1918                            let row = ids[k] as usize;
1919                            if row < axis_dim {
1920                                let tab_row = tab_outer + row * trailing;
1921                                let out_row = out_outer + k * trailing;
1922                                out[out_row..out_row + trailing]
1923                                    .copy_from_slice(&tab[tab_row..tab_row + trailing]);
1924                            }
1925                        }
1926                    }
1927                }
1928            }
1929        }
1930    }
1931}
1932
1933#[inline(always)]
1934pub(crate) fn exec_reverse(t: &Thunk, base: *mut u8) {
1935    let Thunk::Reverse {
1936        src,
1937        dst,
1938        dims,
1939        rev_mask,
1940        elem_bytes,
1941    } = t
1942    else {
1943        unreachable!()
1944    };
1945    {
1946        let eb = *elem_bytes as usize;
1947        let rank = dims.len();
1948        let total: usize = dims.iter().map(|&d| d as usize).product::<usize>().max(1);
1949        let mut strides = vec![1usize; rank];
1950        for i in (0..rank.saturating_sub(1)).rev() {
1951            strides[i] = strides[i + 1] * dims[i + 1] as usize;
1952        }
1953        unsafe {
1954            let src_base = base.add(*src);
1955            let dst_base = base.add(*dst);
1956            for o in 0..total {
1957                let mut rem = o;
1958                let mut in_flat = 0usize;
1959                for ax in 0..rank {
1960                    let idx = rem / strides[ax];
1961                    rem %= strides[ax];
1962                    let in_idx = if rev_mask[ax] {
1963                        dims[ax] as usize - 1 - idx
1964                    } else {
1965                        idx
1966                    };
1967                    in_flat += in_idx * strides[ax];
1968                }
1969                std::ptr::copy_nonoverlapping(src_base.add(in_flat * eb), dst_base.add(o * eb), eb);
1970            }
1971        }
1972    }
1973}
1974
1975/// Host-fallback entry for `Op::GatedDeltaNet` (Metal / unified memory).
1976/// When `state == 0`, uses a zero-initialized scratch state per batch item.
1977/// Batch-general reverse/flip (dtype-agnostic, byte-copy). Shared by the CPU
1978/// `Thunk::Reverse` arm and the Metal/WGPU host paths. `dims` is the row-major
1979/// input shape; `rev_mask[a]` flips axis `a`. `src`/`dst` are byte offsets.
1980pub unsafe fn execute_reverse(
1981    src: usize,
1982    dst: usize,
1983    dims: &[u32],
1984    rev_mask: &[bool],
1985    elem_bytes: usize,
1986    base: *mut u8,
1987) {
1988    let rank = dims.len();
1989    let total: usize = dims.iter().map(|&d| d as usize).product::<usize>().max(1);
1990    let mut strides = vec![1usize; rank];
1991    for i in (0..rank.saturating_sub(1)).rev() {
1992        strides[i] = strides[i + 1] * dims[i + 1] as usize;
1993    }
1994    unsafe {
1995        let src_base = base.add(src);
1996        let dst_base = base.add(dst);
1997        for o in 0..total {
1998            let mut rem = o;
1999            let mut in_flat = 0usize;
2000            for ax in 0..rank {
2001                let idx = rem / strides[ax];
2002                rem %= strides[ax];
2003                let in_idx = if rev_mask[ax] {
2004                    dims[ax] as usize - 1 - idx
2005                } else {
2006                    idx
2007                };
2008                in_flat += in_idx * strides[ax];
2009            }
2010            std::ptr::copy_nonoverlapping(
2011                src_base.add(in_flat * elem_bytes),
2012                dst_base.add(o * elem_bytes),
2013                elem_bytes,
2014            );
2015        }
2016    }
2017}
2018
2019pub unsafe fn execute_gather_backward_f32(
2020    dy: usize,
2021    indices: usize,
2022    dst: usize,
2023    outer: u32,
2024    axis_dim: u32,
2025    num_idx: u32,
2026    trailing: u32,
2027    base: *mut u8,
2028) {
2029    let (outer, axis_dim, num_idx, trailing) = (
2030        outer as usize,
2031        axis_dim as usize,
2032        num_idx as usize,
2033        trailing as usize,
2034    );
2035    let out = sl_mut(dst, base, outer * axis_dim * trailing);
2036    out.fill(0.0);
2037    crate::training_bwd::gather_axis_backward(
2038        sl(dy, base, outer * num_idx * trailing),
2039        sl(indices, base, num_idx),
2040        out,
2041        outer,
2042        axis_dim,
2043        num_idx,
2044        trailing,
2045    );
2046}
2047
2048/// Scalar activation for the fused-region interpreter. Matches the GPU region
2049/// kernel's math (e.g. tanh-approx GELU) so CPU/GPU region results agree.
2050#[inline]
2051pub(crate) fn region_activation_scalar(act: rlx_ir::op::Activation, x: f32) -> f32 {
2052    use rlx_ir::op::Activation as A;
2053    const GC: f32 = 0.797_884_6; // sqrt(2/pi)
2054    match act {
2055        A::Relu => x.max(0.0),
2056        A::Gelu | A::GeluApprox => 0.5 * x * (1.0 + (GC * (x + 0.044715 * x * x * x)).tanh()),
2057        A::Silu => x / (1.0 + (-x).exp()),
2058        A::Sigmoid => 1.0 / (1.0 + (-x).exp()),
2059        A::Tanh => x.tanh(),
2060        A::Exp => x.exp(),
2061        A::Log => x.ln(),
2062        A::Sqrt => x.sqrt(),
2063        A::Rsqrt => 1.0 / x.sqrt(),
2064        A::Neg => -x,
2065        A::Abs => x.abs(),
2066        A::Sin => x.sin(),
2067        A::Cos => x.cos(),
2068        A::Tan => x.tan(),
2069        A::Atan => x.atan(),
2070        A::Round => x.round(),
2071    }
2072}
2073
2074#[inline]
2075pub(crate) fn region_binary_scalar(op: rlx_ir::op::BinaryOp, l: f32, r: f32) -> f32 {
2076    use rlx_ir::op::BinaryOp as B;
2077    match op {
2078        B::Add => l + r,
2079        B::Sub => l - r,
2080        B::Mul => l * r,
2081        B::Div => l / r,
2082        B::Max => l.max(r),
2083        B::Min => l.min(r),
2084        B::Pow => l.powf(r),
2085    }
2086}
2087
2088#[inline]
2089pub(crate) fn region_compare_scalar(op: rlx_ir::op::CmpOp, l: f32, r: f32) -> bool {
2090    use rlx_ir::op::CmpOp as C;
2091    match op {
2092        C::Eq => l == r,
2093        C::Ne => l != r,
2094        C::Lt => l < r,
2095        C::Le => l <= r,
2096        C::Gt => l > r,
2097        C::Ge => l >= r,
2098    }
2099}
2100
2101/// Resolve one chain operand for output element `gid`: a previous step result
2102/// (`scratch[s]`) or an external input read with broadcast (`input[gid % mod]`,
2103/// scalar inputs read element 0). `base` is the arena byte base.
2104#[inline]
2105pub(crate) fn region_resolve_operand(
2106    op: &rlx_ir::op::ChainOperand,
2107    gid: usize,
2108    base: *const u8,
2109    input_offs: &[usize],
2110    scalar_mask: u32,
2111    modulus: &[u32; 16],
2112    scratch: &[f32; 32],
2113) -> f32 {
2114    use rlx_ir::op::ChainOperand as O;
2115    match op {
2116        O::Step(s) => scratch[*s as usize],
2117        O::Input(i) => {
2118            let i = *i as usize;
2119            let row = if (scalar_mask >> i) & 1 == 1 {
2120                0
2121            } else if modulus[i] != 0 {
2122                gid % modulus[i] as usize
2123            } else {
2124                gid
2125            };
2126            unsafe { *(base.add(input_offs[i]) as *const f32).add(row) }
2127        }
2128    }
2129}
2130
2131/// Evaluate a fused element-wise chain for output element `gid`, returning the
2132/// final step's value (what gets written to `dst[gid]`).
2133#[inline]
2134pub(crate) fn region_eval_elem(
2135    gid: usize,
2136    base: *const u8,
2137    input_offs: &[usize],
2138    chain: &[rlx_ir::op::ChainStep],
2139    scalar_mask: u32,
2140    modulus: &[u32; 16],
2141) -> f32 {
2142    use rlx_ir::op::ChainStep as S;
2143    let mut scratch = [0f32; 32];
2144    let r = |o: &rlx_ir::op::ChainOperand, sc: &[f32; 32]| {
2145        region_resolve_operand(o, gid, base, input_offs, scalar_mask, modulus, sc)
2146    };
2147    for (k, step) in chain.iter().enumerate() {
2148        scratch[k] = match step {
2149            S::Activation(a, x) => region_activation_scalar(*a, r(x, &scratch)),
2150            S::Cast(_, x) => r(x, &scratch), // f32→f32 identity (chains are same-dtype)
2151            S::Binary(op, l, rr) => region_binary_scalar(*op, r(l, &scratch), r(rr, &scratch)),
2152            S::Compare(op, l, rr) => {
2153                if region_compare_scalar(*op, r(l, &scratch), r(rr, &scratch)) {
2154                    1.0
2155                } else {
2156                    0.0
2157                }
2158            }
2159            S::Where(c, t, f) => {
2160                if r(c, &scratch) != 0.0 {
2161                    r(t, &scratch)
2162                } else {
2163                    r(f, &scratch)
2164                }
2165            }
2166        };
2167    }
2168    scratch[chain.len() - 1]
2169}
2170
2171// Unsafe helpers to create slices from arena base + offset
2172/// In-place per-element activation. Mirrors the dispatch in
2173/// `Thunk::ActivationInPlace`. Used by `Thunk::FusedMmBiasAct` to
2174/// apply the activation after `bias_add` for all non-Gelu cases.
2175#[inline(always)]
2176pub(crate) fn apply_activation_inplace(d: &mut [f32], act: rlx_ir::op::Activation) {
2177    use rlx_ir::op::Activation;
2178    match act {
2179        Activation::Gelu => crate::kernels::par_gelu_inplace(d),
2180        Activation::GeluApprox => crate::kernels::par_gelu_approx_inplace(d),
2181        Activation::Silu => crate::kernels::par_silu_inplace(d),
2182        Activation::Relu => {
2183            for v in d.iter_mut() {
2184                *v = v.max(0.0);
2185            }
2186        }
2187        Activation::Sigmoid => {
2188            for v in d.iter_mut() {
2189                *v = 1.0 / (1.0 + (-*v).exp());
2190            }
2191        }
2192        Activation::Tanh => {
2193            for v in d.iter_mut() {
2194                *v = v.tanh();
2195            }
2196        }
2197        Activation::Exp => {
2198            for v in d.iter_mut() {
2199                *v = v.exp();
2200            }
2201        }
2202        Activation::Log => {
2203            for v in d.iter_mut() {
2204                *v = v.ln();
2205            }
2206        }
2207        Activation::Sqrt => {
2208            for v in d.iter_mut() {
2209                *v = v.sqrt();
2210            }
2211        }
2212        Activation::Rsqrt => {
2213            for v in d.iter_mut() {
2214                *v = 1.0 / v.sqrt();
2215            }
2216        }
2217        Activation::Neg => {
2218            for v in d.iter_mut() {
2219                *v = -*v;
2220            }
2221        }
2222        Activation::Abs => {
2223            for v in d.iter_mut() {
2224                *v = v.abs();
2225            }
2226        }
2227        Activation::Round => {
2228            for v in d.iter_mut() {
2229                *v = v.round();
2230            }
2231        }
2232        Activation::Sin => {
2233            for v in d.iter_mut() {
2234                *v = v.sin();
2235            }
2236        }
2237        Activation::Cos => {
2238            for v in d.iter_mut() {
2239                *v = v.cos();
2240            }
2241        }
2242        Activation::Tan => {
2243            for v in d.iter_mut() {
2244                *v = v.tan();
2245            }
2246        }
2247        Activation::Atan => {
2248            for v in d.iter_mut() {
2249                *v = v.atan();
2250            }
2251        }
2252    }
2253}
2254
2255pub(crate) fn activation_backward_kernel(
2256    act: rlx_ir::op::Activation,
2257    xs: &[f32],
2258    dys: &[f32],
2259    out: &mut [f32],
2260) {
2261    use rlx_ir::op::Activation;
2262    let n = xs.len();
2263    debug_assert_eq!(dys.len(), n);
2264    debug_assert_eq!(out.len(), n);
2265    match act {
2266        Activation::Relu => {
2267            for i in 0..n {
2268                out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
2269            }
2270        }
2271        Activation::Sigmoid => {
2272            for i in 0..n {
2273                let s = 1.0 / (1.0 + (-xs[i]).exp());
2274                out[i] = s * (1.0 - s) * dys[i];
2275            }
2276        }
2277        Activation::Tanh => {
2278            for i in 0..n {
2279                let t = xs[i].tanh();
2280                out[i] = (1.0 - t * t) * dys[i];
2281            }
2282        }
2283        Activation::Silu => {
2284            // y = x * σ(x);  dy/dx = σ(x) * (1 + x * (1 - σ(x))).
2285            for i in 0..n {
2286                let s = 1.0 / (1.0 + (-xs[i]).exp());
2287                out[i] = s * (1.0 + xs[i] * (1.0 - s)) * dys[i];
2288            }
2289        }
2290        Activation::Gelu => {
2291            // Exact erf-based GELU:  y = 0.5 x (1 + erf(x / √2)).
2292            //   dy/dx = 0.5 (1 + erf(x/√2)) + (x / √(2π)) · exp(-x²/2)
2293            const INV_SQRT2: f32 = 0.707_106_77;
2294            const INV_SQRT_2PI: f32 = 0.398_942_3;
2295            for i in 0..n {
2296                let x = xs[i];
2297                let phi = 0.5 * (1.0 + erf_f32(x * INV_SQRT2));
2298                let pdf = INV_SQRT_2PI * (-(x * x) * 0.5).exp();
2299                out[i] = (phi + x * pdf) * dys[i];
2300            }
2301        }
2302        Activation::GeluApprox => {
2303            // Tanh-approximation:
2304            //   y = 0.5 x (1 + tanh(c · (x + 0.044715 x³))) where c = √(2/π).
2305            const C: f32 = 0.797_884_6; // √(2/π)
2306            const A: f32 = 0.044_715;
2307            for i in 0..n {
2308                let x = xs[i];
2309                let inner = C * (x + A * x * x * x);
2310                let t = inner.tanh();
2311                let dinner = C * (1.0 + 3.0 * A * x * x);
2312                let d = 0.5 * (1.0 + t) + 0.5 * x * (1.0 - t * t) * dinner;
2313                out[i] = d * dys[i];
2314            }
2315        }
2316        Activation::Exp => {
2317            for i in 0..n {
2318                out[i] = xs[i].exp() * dys[i];
2319            }
2320        }
2321        Activation::Log => {
2322            for i in 0..n {
2323                out[i] = dys[i] / xs[i];
2324            }
2325        }
2326        Activation::Sqrt => {
2327            // d/dx √x = 0.5 / √x — undefined at x=0; clamp to 0.
2328            for i in 0..n {
2329                let s = xs[i].sqrt();
2330                out[i] = if s > 0.0 { 0.5 * dys[i] / s } else { 0.0 };
2331            }
2332        }
2333        Activation::Rsqrt => {
2334            // d/dx (1/√x) = -0.5 · x^(-3/2).
2335            for i in 0..n {
2336                let s = xs[i].sqrt();
2337                out[i] = if s > 0.0 {
2338                    -0.5 * dys[i] / (xs[i] * s)
2339                } else {
2340                    0.0
2341                };
2342            }
2343        }
2344        Activation::Neg => {
2345            for i in 0..n {
2346                out[i] = -dys[i];
2347            }
2348        }
2349        Activation::Abs => {
2350            // sign(x); 0 at x=0.
2351            for i in 0..n {
2352                let x = xs[i];
2353                let s = if x > 0.0 {
2354                    1.0
2355                } else if x < 0.0 {
2356                    -1.0
2357                } else {
2358                    0.0
2359                };
2360                out[i] = s * dys[i];
2361            }
2362        }
2363        Activation::Round => {
2364            // STE: pretend the round was identity in the backward
2365            // pass. The round step has zero gradient almost
2366            // everywhere, so without this trick the optimizer can't
2367            // learn through it.
2368            out.copy_from_slice(dys);
2369        }
2370        Activation::Sin => {
2371            // d/dx sin(x) = cos(x).
2372            for i in 0..n {
2373                out[i] = xs[i].cos() * dys[i];
2374            }
2375        }
2376        Activation::Cos => {
2377            for i in 0..n {
2378                out[i] = -xs[i].sin() * dys[i];
2379            }
2380        }
2381        Activation::Tan => {
2382            // d/dx tan(x) = sec²(x) = 1 + tan²(x)
2383            for i in 0..n {
2384                let t = xs[i].tan();
2385                out[i] = (1.0 + t * t) * dys[i];
2386            }
2387        }
2388        Activation::Atan => {
2389            // d/dx atan(x) = 1 / (1 + x²)
2390            for i in 0..n {
2391                let x = xs[i];
2392                out[i] = dys[i] / (1.0 + x * x);
2393            }
2394        }
2395    }
2396}
2397
2398/// f64 sibling of `activation_backward_kernel`. Same math, twice the
2399/// precision — used by f64 graphs where the f32 kernel reading bytes
2400/// as `&[f32]` would silently discard half of every f64 value.
2401pub(crate) fn activation_backward_kernel_f64(
2402    act: rlx_ir::op::Activation,
2403    xs: &[f64],
2404    dys: &[f64],
2405    out: &mut [f64],
2406) {
2407    use rlx_ir::op::Activation;
2408    let n = xs.len();
2409    debug_assert_eq!(dys.len(), n);
2410    debug_assert_eq!(out.len(), n);
2411    match act {
2412        Activation::Relu => {
2413            for i in 0..n {
2414                out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
2415            }
2416        }
2417        Activation::Sigmoid => {
2418            for i in 0..n {
2419                let s = 1.0 / (1.0 + (-xs[i]).exp());
2420                out[i] = s * (1.0 - s) * dys[i];
2421            }
2422        }
2423        Activation::Tanh => {
2424            for i in 0..n {
2425                let t = xs[i].tanh();
2426                out[i] = (1.0 - t * t) * dys[i];
2427            }
2428        }
2429        Activation::Silu => {
2430            for i in 0..n {
2431                let s = 1.0 / (1.0 + (-xs[i]).exp());
2432                out[i] = s * (1.0 + xs[i] * (1.0 - s)) * dys[i];
2433            }
2434        }
2435        Activation::Gelu | Activation::GeluApprox => {
2436            // Both rare on f64 paths; use the high-quality libm erf.
2437            const INV_SQRT2: f64 = std::f64::consts::FRAC_1_SQRT_2;
2438            const INV_SQRT_2PI: f64 = 0.398_942_280_401_432_7;
2439            for i in 0..n {
2440                let x = xs[i];
2441                let phi = 0.5 * (1.0 + erf_f64(x * INV_SQRT2));
2442                let pdf = INV_SQRT_2PI * (-(x * x) * 0.5).exp();
2443                out[i] = (phi + x * pdf) * dys[i];
2444            }
2445        }
2446        Activation::Exp => {
2447            for i in 0..n {
2448                out[i] = xs[i].exp() * dys[i];
2449            }
2450        }
2451        Activation::Log => {
2452            for i in 0..n {
2453                out[i] = dys[i] / xs[i];
2454            }
2455        }
2456        Activation::Sqrt => {
2457            for i in 0..n {
2458                let s = xs[i].sqrt();
2459                out[i] = if s > 0.0 { 0.5 * dys[i] / s } else { 0.0 };
2460            }
2461        }
2462        Activation::Rsqrt => {
2463            for i in 0..n {
2464                let s = xs[i].sqrt();
2465                out[i] = if s > 0.0 {
2466                    -0.5 * dys[i] / (xs[i] * s)
2467                } else {
2468                    0.0
2469                };
2470            }
2471        }
2472        Activation::Neg => {
2473            for i in 0..n {
2474                out[i] = -dys[i];
2475            }
2476        }
2477        Activation::Abs => {
2478            for i in 0..n {
2479                let x = xs[i];
2480                let s = if x > 0.0 {
2481                    1.0
2482                } else if x < 0.0 {
2483                    -1.0
2484                } else {
2485                    0.0
2486                };
2487                out[i] = s * dys[i];
2488            }
2489        }
2490        Activation::Round => {
2491            out.copy_from_slice(dys);
2492        }
2493        Activation::Sin => {
2494            for i in 0..n {
2495                out[i] = xs[i].cos() * dys[i];
2496            }
2497        }
2498        Activation::Cos => {
2499            for i in 0..n {
2500                out[i] = -xs[i].sin() * dys[i];
2501            }
2502        }
2503        Activation::Tan => {
2504            for i in 0..n {
2505                let t = xs[i].tan();
2506                out[i] = (1.0 + t * t) * dys[i];
2507            }
2508        }
2509        Activation::Atan => {
2510            for i in 0..n {
2511                let x = xs[i];
2512                out[i] = dys[i] / (1.0 + x * x);
2513            }
2514        }
2515    }
2516}
2517
2518/// f64 erf via A&S 7.1.26 — same coefficients as `erf_f32`, computed
2519/// at f64 width. Max error ~1.5e-7 (limited by the polynomial, not the
2520/// arithmetic). Adequate for gradient kernels; if higher precision is
2521/// needed, swap in a libm dependency.
2522#[inline(always)]
2523pub(crate) fn erf_f64(x: f64) -> f64 {
2524    let s = x.signum();
2525    let x = x.abs();
2526    let t = 1.0 / (1.0 + 0.327_591_1 * x);
2527    let y = 1.0
2528        - (((((1.061_405_43 * t - 1.453_152_03) * t) + 1.421_413_75) * t - 0.284_496_74) * t
2529            + 0.254_829_59)
2530            * t
2531            * (-x * x).exp();
2532    s * y
2533}
2534
2535/// Cheap erf approximation (Abramowitz & Stegun 7.1.26, max error ~1.5e-7
2536/// over all of ℝ — plenty for f32 gradient kernels).
2537#[inline(always)]
2538pub(crate) fn erf_f32(x: f32) -> f32 {
2539    let s = x.signum();
2540    let x = x.abs();
2541    let t = 1.0 / (1.0 + 0.327_591_1 * x);
2542    let y = 1.0
2543        - (((((1.061_405_4 * t - 1.453_152_1) * t) + 1.421_413_8) * t - 0.284_496_74) * t
2544            + 0.254_829_6)
2545            * t
2546            * (-x * x).exp();
2547    s * y
2548}
2549
2550pub(crate) fn narrow_thunk_closure(
2551    src: usize,
2552    dst: usize,
2553    outer: u32,
2554    src_stride: u32,
2555    dst_stride: u32,
2556    inner: u32,
2557    elem_bytes: u8,
2558) -> Arc<dyn Fn(*mut u8) + Send + Sync> {
2559    let (outer, ss, ds, inner, eb) = (
2560        outer as usize,
2561        src_stride as usize,
2562        dst_stride as usize,
2563        inner as usize,
2564        elem_bytes as usize,
2565    );
2566    let row_bytes = inner.saturating_mul(eb);
2567    let src_row_stride = ss.saturating_mul(eb);
2568    let dst_row_stride = ds.saturating_mul(eb);
2569    Arc::new(move |base: *mut u8| unsafe {
2570        if row_bytes == 0 || src == dst {
2571            return;
2572        }
2573        // Compiled-fn path has no arena length; skip if offsets look bogus.
2574        let arena_len = usize::MAX;
2575        for o in 0..outer {
2576            let s_off = src + o * src_row_stride;
2577            let d_off = dst + o * dst_row_stride;
2578            if s_off == d_off {
2579                continue;
2580            }
2581            if s_off.saturating_add(row_bytes) > arena_len
2582                || d_off.saturating_add(row_bytes) > arena_len
2583            {
2584                break;
2585            }
2586            std::ptr::copy_nonoverlapping(base.add(s_off), base.add(d_off), row_bytes);
2587        }
2588    })
2589}
2590
2591/// f64 N-D index walk used by Transpose and Expand. `out_dims` gives
2592/// the output shape; `in_strides` gives the source stride for each
2593/// output dim (broadcast axes have stride 0).
2594pub(crate) fn transpose_walk_f64(
2595    inp: &[f64],
2596    out: &mut [f64],
2597    out_dims: &[u32],
2598    in_strides: &[u32],
2599) {
2600    let rank = out_dims.len();
2601    let mut idx = vec![0u32; rank];
2602    for o in 0..out.len() {
2603        let mut src_off = 0usize;
2604        for d in 0..rank {
2605            src_off += idx[d] as usize * in_strides[d] as usize;
2606        }
2607        out[o] = inp[broadcast_src_index(src_off, inp.len())];
2608        // Increment index — last dim varies fastest.
2609        for d in (0..rank).rev() {
2610            idx[d] += 1;
2611            if idx[d] < out_dims[d] {
2612                break;
2613            }
2614            idx[d] = 0;
2615        }
2616    }
2617}
2618
2619/// f64 elementwise activation. Reads `inp`, writes `out`. For now
2620/// covers what the autodiff-emitted gradient graph needs (Neg, Exp,
2621/// Log, Sqrt, Rsqrt, Abs, Tanh, Sigmoid, Relu — the
2622/// transcendental-free subset). Approximate Gelu/Silu deferred until a
2623/// workload demands them at f64.
2624pub(crate) fn apply_activation_f64(inp: &[f64], out: &mut [f64], kind: Activation) {
2625    match kind {
2626        Activation::Neg => {
2627            for (o, &v) in out.iter_mut().zip(inp) {
2628                *o = -v;
2629            }
2630        }
2631        Activation::Exp => {
2632            for (o, &v) in out.iter_mut().zip(inp) {
2633                *o = v.exp();
2634            }
2635        }
2636        Activation::Log => {
2637            for (o, &v) in out.iter_mut().zip(inp) {
2638                *o = v.ln();
2639            }
2640        }
2641        Activation::Sqrt => {
2642            for (o, &v) in out.iter_mut().zip(inp) {
2643                *o = v.sqrt();
2644            }
2645        }
2646        Activation::Rsqrt => {
2647            for (o, &v) in out.iter_mut().zip(inp) {
2648                *o = 1.0 / v.sqrt();
2649            }
2650        }
2651        Activation::Abs => {
2652            for (o, &v) in out.iter_mut().zip(inp) {
2653                *o = v.abs();
2654            }
2655        }
2656        Activation::Tanh => {
2657            for (o, &v) in out.iter_mut().zip(inp) {
2658                *o = v.tanh();
2659            }
2660        }
2661        Activation::Sigmoid => {
2662            for (o, &v) in out.iter_mut().zip(inp) {
2663                *o = 1.0 / (1.0 + (-v).exp());
2664            }
2665        }
2666        Activation::Relu => {
2667            for (o, &v) in out.iter_mut().zip(inp) {
2668                *o = v.max(0.0);
2669            }
2670        }
2671        Activation::Round => {
2672            for (o, &v) in out.iter_mut().zip(inp) {
2673                *o = v.round_ties_even();
2674            }
2675        }
2676        Activation::Sin => {
2677            for (o, &v) in out.iter_mut().zip(inp) {
2678                *o = v.sin();
2679            }
2680        }
2681        Activation::Cos => {
2682            for (o, &v) in out.iter_mut().zip(inp) {
2683                *o = v.cos();
2684            }
2685        }
2686        Activation::Tan => {
2687            for (o, &v) in out.iter_mut().zip(inp) {
2688                *o = v.tan();
2689            }
2690        }
2691        Activation::Atan => {
2692            for (o, &v) in out.iter_mut().zip(inp) {
2693                *o = v.atan();
2694            }
2695        }
2696        Activation::Gelu | Activation::GeluApprox | Activation::Silu => {
2697            panic!(
2698                "apply_activation_f64: {kind:?} not yet implemented at f64. \
2699                    Add when a workload needs it."
2700            );
2701        }
2702    }
2703}
2704
2705#[inline]
2706pub(crate) fn binary_op_f64(op: BinaryOp, a: f64, b: f64) -> f64 {
2707    match op {
2708        BinaryOp::Add => a + b,
2709        BinaryOp::Sub => a - b,
2710        BinaryOp::Mul => a * b,
2711        BinaryOp::Div => a / b,
2712        BinaryOp::Max => a.max(b),
2713        BinaryOp::Min => a.min(b),
2714        BinaryOp::Pow => a.powf(b),
2715    }
2716}