Skip to main content

rlx_cpu/thunk/ops/
rnn.rs

1#![allow(unsafe_op_in_unsafe_fn)]
2use crate::thunk::*;
3
4/// Execute a compiled scan body over `length` steps against a raw arena buffer
5/// `base`. `outer_init_off` / `outer_final_off` are byte offsets into that
6/// arena; `xs_outer[i] = (outer_off, per_step_bytes)` and
7/// `bcast_outer[i] = (outer_off, total_bytes)` align with the plan's
8/// `xs_body_offs` / `bcast_body_offs`. When `save_trajectory`, row `t` is
9/// written to `outer_final_off + t * carry_bytes` (every step; checkpointed
10/// scans are not supported via this path).
11///
12/// # Safety
13/// `base` must point to a valid arena spanning every referenced offset.
14pub unsafe fn execute_scan_host(
15    base: *mut u8,
16    plan: &ScanBodyPlan,
17    outer_init_off: usize,
18    outer_final_off: usize,
19    length: u32,
20    save_trajectory: bool,
21    xs_outer: &[(usize, usize)],
22    bcast_outer: &[(usize, usize)],
23) {
24    let cb = plan.carry_bytes;
25    let mut body_buf: Vec<u8> = plan.body_init.clone();
26    unsafe {
27        std::ptr::copy_nonoverlapping(
28            base.add(outer_init_off),
29            body_buf.as_mut_ptr().add(plan.body_input_off),
30            cb,
31        );
32        for (i, &(outer_b_off, total)) in bcast_outer.iter().enumerate() {
33            std::ptr::copy_nonoverlapping(
34                base.add(outer_b_off),
35                body_buf.as_mut_ptr().add(plan.bcast_body_offs[i]),
36                total,
37            );
38        }
39    }
40    for t in 0..length as usize {
41        for (i, &(outer_xs_off, psb)) in xs_outer.iter().enumerate() {
42            unsafe {
43                std::ptr::copy_nonoverlapping(
44                    base.add(outer_xs_off + t * psb),
45                    body_buf.as_mut_ptr().add(plan.xs_body_offs[i]),
46                    psb,
47                );
48            }
49        }
50        execute_thunks(&plan.body, &mut body_buf);
51        if save_trajectory {
52            unsafe {
53                std::ptr::copy_nonoverlapping(
54                    body_buf.as_ptr().add(plan.body_output_off),
55                    base.add(outer_final_off + t * cb),
56                    cb,
57                );
58            }
59        }
60        if plan.body_output_off != plan.body_input_off {
61            body_buf.copy_within(
62                plan.body_output_off..plan.body_output_off + cb,
63                plan.body_input_off,
64            );
65        }
66    }
67    if !save_trajectory {
68        unsafe {
69            std::ptr::copy_nonoverlapping(
70                body_buf.as_ptr().add(plan.body_output_off),
71                base.add(outer_final_off),
72                cb,
73            );
74        }
75    }
76}
77
78#[allow(unused_variables)]
79pub(crate) fn compile_selective_scan(
80    node: &rlx_ir::Node,
81    graph: &Graph,
82    arena: &crate::arena::Arena,
83    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
84    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
85    rng: rlx_ir::RngOptions,
86) -> Thunk {
87    let Op::SelectiveScan { state_size } = &node.op else {
88        unreachable!()
89    };
90    {
91        let in_shape = &graph.node(node.inputs[0]).shape;
92        let (batch, seq, hidden) = (
93            in_shape.dim(0).unwrap_static(),
94            in_shape.dim(1).unwrap_static(),
95            in_shape.dim(2).unwrap_static(),
96        );
97        Thunk::SelectiveScan {
98            x: node_offset(arena, node.inputs[0]),
99            delta: node_offset(arena, node.inputs[1]),
100            a: node_offset(arena, node.inputs[2]),
101            b: node_offset(arena, node.inputs[3]),
102            c: node_offset(arena, node.inputs[4]),
103            dst: node_offset(arena, node.id),
104            batch: batch as u32,
105            seq: seq as u32,
106            hidden: hidden as u32,
107            state_size: *state_size as u32,
108        }
109    }
110}
111
112#[allow(unused_variables)]
113pub(crate) fn compile_gated_delta_net(
114    node: &rlx_ir::Node,
115    graph: &Graph,
116    arena: &crate::arena::Arena,
117    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
118    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
119    rng: rlx_ir::RngOptions,
120) -> Thunk {
121    let Op::GatedDeltaNet {
122        state_size,
123        carry_state,
124    } = &node.op
125    else {
126        unreachable!()
127    };
128    {
129        let q_shape = &graph.node(node.inputs[0]).shape;
130        let (batch, seq, heads) = (
131            q_shape.dim(0).unwrap_static(),
132            q_shape.dim(1).unwrap_static(),
133            q_shape.dim(2).unwrap_static(),
134        );
135        let state_off = if *carry_state {
136            node_offset(arena, node.inputs[5])
137        } else {
138            0
139        };
140        Thunk::GatedDeltaNet {
141            q: node_offset(arena, node.inputs[0]),
142            k: node_offset(arena, node.inputs[1]),
143            v: node_offset(arena, node.inputs[2]),
144            g: node_offset(arena, node.inputs[3]),
145            beta: node_offset(arena, node.inputs[4]),
146            state: state_off,
147            dst: node_offset(arena, node.id),
148            batch: batch as u32,
149            seq: seq as u32,
150            heads: heads as u32,
151            state_size: *state_size as u32,
152        }
153    }
154}
155
156#[allow(unused_variables)]
157pub(crate) fn compile_lstm(
158    node: &rlx_ir::Node,
159    graph: &Graph,
160    arena: &crate::arena::Arena,
161    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
162    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
163    rng: rlx_ir::RngOptions,
164) -> Thunk {
165    let Op::Lstm {
166        hidden_size,
167        num_layers,
168        bidirectional,
169        carry,
170    } = &node.op
171    else {
172        unreachable!()
173    };
174    {
175        let x_shape = &graph.node(node.inputs[0]).shape;
176        let (batch, seq, input_size) = (
177            x_shape.dim(0).unwrap_static(),
178            x_shape.dim(1).unwrap_static(),
179            x_shape.dim(2).unwrap_static(),
180        );
181        let ex = rnn_expected_lens(
182            4,
183            batch,
184            seq,
185            input_size,
186            *hidden_size,
187            *num_layers,
188            *bidirectional,
189        );
190        let nelem = |i: usize| graph.node(node.inputs[i]).shape.num_elements().unwrap_or(0);
191        check_rnn_lens(
192            "lstm",
193            &[
194                ("w_ih", nelem(1), ex.w_ih),
195                ("w_hh", nelem(2), ex.w_hh),
196                ("bias", nelem(3), ex.bias),
197            ],
198        )
199        .unwrap_or_else(|e| panic!("{e}"));
200        let (h0, c0) = if *carry {
201            (
202                node_offset(arena, node.inputs[4]),
203                node_offset(arena, node.inputs[5]),
204            )
205        } else {
206            (0, 0)
207        };
208        Thunk::Lstm {
209            x: node_offset(arena, node.inputs[0]),
210            w_ih: node_offset(arena, node.inputs[1]),
211            w_hh: node_offset(arena, node.inputs[2]),
212            bias: node_offset(arena, node.inputs[3]),
213            h0,
214            c0,
215            dst: node_offset(arena, node.id),
216            batch: batch as u32,
217            seq: seq as u32,
218            input_size: input_size as u32,
219            hidden: *hidden_size as u32,
220            num_layers: *num_layers as u32,
221            bidirectional: *bidirectional,
222            carry: *carry,
223        }
224    }
225}
226
227#[allow(unused_variables)]
228pub(crate) fn compile_gru(
229    node: &rlx_ir::Node,
230    graph: &Graph,
231    arena: &crate::arena::Arena,
232    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
233    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
234    rng: rlx_ir::RngOptions,
235) -> Thunk {
236    let Op::Gru {
237        hidden_size,
238        num_layers,
239        bidirectional,
240        carry,
241    } = &node.op
242    else {
243        unreachable!()
244    };
245    {
246        let x_shape = &graph.node(node.inputs[0]).shape;
247        let (batch, seq, input_size) = (
248            x_shape.dim(0).unwrap_static(),
249            x_shape.dim(1).unwrap_static(),
250            x_shape.dim(2).unwrap_static(),
251        );
252        // Inputs: x, w_ih, w_hh, b_ih, b_hh (+ h0 when carry).
253        let ex = rnn_expected_lens(
254            3,
255            batch,
256            seq,
257            input_size,
258            *hidden_size,
259            *num_layers,
260            *bidirectional,
261        );
262        let nelem = |i: usize| graph.node(node.inputs[i]).shape.num_elements().unwrap_or(0);
263        check_rnn_lens(
264            "gru",
265            &[
266                ("w_ih", nelem(1), ex.w_ih),
267                ("w_hh", nelem(2), ex.w_hh),
268                ("b_ih", nelem(3), ex.bias),
269                ("b_hh", nelem(4), ex.bias),
270            ],
271        )
272        .unwrap_or_else(|e| panic!("{e}"));
273        let h0 = if *carry {
274            node_offset(arena, node.inputs[5])
275        } else {
276            0
277        };
278        Thunk::Gru {
279            x: node_offset(arena, node.inputs[0]),
280            w_ih: node_offset(arena, node.inputs[1]),
281            w_hh: node_offset(arena, node.inputs[2]),
282            b_ih: node_offset(arena, node.inputs[3]),
283            b_hh: node_offset(arena, node.inputs[4]),
284            h0,
285            dst: node_offset(arena, node.id),
286            batch: batch as u32,
287            seq: seq as u32,
288            input_size: input_size as u32,
289            hidden: *hidden_size as u32,
290            num_layers: *num_layers as u32,
291            bidirectional: *bidirectional,
292            carry: *carry,
293        }
294    }
295}
296
297#[allow(unused_variables)]
298pub(crate) fn compile_rnn(
299    node: &rlx_ir::Node,
300    graph: &Graph,
301    arena: &crate::arena::Arena,
302    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
303    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
304    rng: rlx_ir::RngOptions,
305) -> Thunk {
306    let Op::Rnn {
307        hidden_size,
308        num_layers,
309        bidirectional,
310        carry,
311        relu,
312    } = &node.op
313    else {
314        unreachable!()
315    };
316    {
317        let x_shape = &graph.node(node.inputs[0]).shape;
318        let (batch, seq, input_size) = (
319            x_shape.dim(0).unwrap_static(),
320            x_shape.dim(1).unwrap_static(),
321            x_shape.dim(2).unwrap_static(),
322        );
323        // Inputs: x, w_ih, w_hh, bias (+ h0 when carry).
324        let ex = rnn_expected_lens(
325            1,
326            batch,
327            seq,
328            input_size,
329            *hidden_size,
330            *num_layers,
331            *bidirectional,
332        );
333        let nelem = |i: usize| graph.node(node.inputs[i]).shape.num_elements().unwrap_or(0);
334        check_rnn_lens(
335            "rnn",
336            &[
337                ("w_ih", nelem(1), ex.w_ih),
338                ("w_hh", nelem(2), ex.w_hh),
339                ("bias", nelem(3), ex.bias),
340            ],
341        )
342        .unwrap_or_else(|e| panic!("{e}"));
343        let h0 = if *carry {
344            node_offset(arena, node.inputs[4])
345        } else {
346            0
347        };
348        Thunk::Rnn {
349            x: node_offset(arena, node.inputs[0]),
350            w_ih: node_offset(arena, node.inputs[1]),
351            w_hh: node_offset(arena, node.inputs[2]),
352            bias: node_offset(arena, node.inputs[3]),
353            h0,
354            dst: node_offset(arena, node.id),
355            batch: batch as u32,
356            seq: seq as u32,
357            input_size: input_size as u32,
358            hidden: *hidden_size as u32,
359            num_layers: *num_layers as u32,
360            bidirectional: *bidirectional,
361            carry: *carry,
362            relu: *relu,
363        }
364    }
365}
366
367#[allow(unused_variables)]
368pub(crate) fn compile_mamba2(
369    node: &rlx_ir::Node,
370    graph: &Graph,
371    arena: &crate::arena::Arena,
372    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
373    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
374    rng: rlx_ir::RngOptions,
375) -> Thunk {
376    let Op::Mamba2 {
377        head_dim,
378        state_size,
379    } = &node.op
380    else {
381        unreachable!()
382    };
383    {
384        // x [B,S,H,P]; dt [B,S,H]; a [H]; b,c [B,S,H,N].
385        let x_shape = &graph.node(node.inputs[0]).shape;
386        Thunk::Mamba2 {
387            x: node_offset(arena, node.inputs[0]),
388            dt: node_offset(arena, node.inputs[1]),
389            a: node_offset(arena, node.inputs[2]),
390            b: node_offset(arena, node.inputs[3]),
391            c: node_offset(arena, node.inputs[4]),
392            dst: node_offset(arena, node.id),
393            batch: x_shape.dim(0).unwrap_static() as u32,
394            seq: x_shape.dim(1).unwrap_static() as u32,
395            heads: x_shape.dim(2).unwrap_static() as u32,
396            head_dim: *head_dim as u32,
397            state_size: *state_size as u32,
398        }
399    }
400}
401
402#[allow(unused_variables)]
403pub(crate) fn compile_scan(
404    node: &rlx_ir::Node,
405    graph: &Graph,
406    arena: &crate::arena::Arena,
407    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
408    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
409    rng: rlx_ir::RngOptions,
410) -> Thunk {
411    let Op::Scan {
412        body,
413        length,
414        save_trajectory,
415        num_bcast,
416        num_xs,
417        num_checkpoints,
418    } = &node.op
419    else {
420        unreachable!()
421    };
422    {
423        assert!(
424            *num_checkpoints == 0 || *num_checkpoints <= *length,
425            "Op::Scan: num_checkpoints={} must be 0 or ≤ length={}",
426            *num_checkpoints,
427            *length
428        );
429        if *num_checkpoints != 0 && *num_checkpoints != *length {
430            assert!(
431                *save_trajectory,
432                "Op::Scan: num_checkpoints<length only meaningful when save_trajectory=true"
433            );
434        }
435        // Plan + compile the body sub-graph standalone. The body
436        // gets its own Arena; per execution we clone its
437        // pristine bytes, copy the outer carry (and per-step xs
438        // slices, if any) into the body's Input slots, run the
439        // body schedule N times, then copy the body's output
440        // back to the outer arena.
441        //
442        // Body invariants: 1 + num_xs Op::Inputs in NodeId order
443        // — first declared is the carry, rest are x_t_i. Single
444        // graph output (the next carry), same shape as carry.
445        let body_plan = rlx_opt::memory::plan_memory(body);
446        let _body_arena_size = body_plan.arena_size;
447        // Snapshot per-input byte offsets before plan_memory
448        // moves into the Arena below.
449        let body_offsets: HashMap<NodeId, usize> = body_plan
450            .assignments
451            .iter()
452            .map(|(id, slot)| (*id, slot.offset))
453            .collect();
454
455        // Collect body Input nodes in NodeId order; first is
456        // carry, rest are per-step xs in matching order.
457        let mut body_inputs: Vec<NodeId> = body
458            .nodes()
459            .iter()
460            .filter(|n| matches!(n.op, Op::Input { .. }))
461            .map(|n| n.id)
462            .collect();
463        body_inputs.sort();
464        let n_body_inputs = body_inputs.len();
465        let expected = 1 + *num_bcast as usize + *num_xs as usize;
466        if n_body_inputs != expected {
467            let names: Vec<String> = body
468                .nodes()
469                .iter()
470                .filter_map(|n| match &n.op {
471                    Op::Input { name } => Some(format!("{}={}", n.id, name)),
472                    _ => None,
473                })
474                .collect();
475            panic!(
476                "Op::Scan body has {} Op::Input nodes; expected {} \
477                            (1 carry + {} bcast + {} xs). Inputs by NodeId: [{}]",
478                n_body_inputs,
479                expected,
480                *num_bcast,
481                *num_xs,
482                names.join(", ")
483            );
484        }
485
486        let body_input_id = body_inputs[0];
487        let body_input_off = body_offsets[&body_input_id];
488        let body_output_id = body
489            .outputs
490            .first()
491            .copied()
492            .expect("Op::Scan body must declare one output");
493        let body_output_off = body_offsets[&body_output_id];
494
495        let mut body_arena = crate::arena::Arena::from_plan(body_plan);
496        // Fill body Constant nodes — mirror the outer-graph logic
497        // in rlx-runtime/src/backend.rs (dtype-aware).
498        for n in body.nodes() {
499            if let Op::Constant { data } = &n.op
500                && body_arena.has_buffer(n.id)
501                && !data.is_empty()
502            {
503                match n.shape.dtype() {
504                    rlx_ir::DType::F64 => {
505                        let off = body_arena.byte_offset(n.id);
506                        let buf = body_arena.raw_buf_mut();
507                        let nbytes = (buf.len() - off).min(data.len());
508                        buf[off..off + nbytes].copy_from_slice(&data[..nbytes]);
509                    }
510                    _ => {
511                        let buf = body_arena.slice_mut(n.id);
512                        let n_floats = data.len() / 4;
513                        let n_lim = buf.len().min(n_floats);
514                        for i in 0..n_lim {
515                            let bytes = [
516                                data[i * 4],
517                                data[i * 4 + 1],
518                                data[i * 4 + 2],
519                                data[i * 4 + 3],
520                            ];
521                            buf[i] = f32::from_le_bytes(bytes);
522                        }
523                    }
524                }
525            }
526        }
527        let body_init = body_arena.raw_buf().to_vec();
528        let body_schedule = compile_thunks_with_rng(body, &body_arena, rng);
529
530        // Carry bytes — for trajectory mode, the outer node's
531        // shape is [length, *carry_shape], so dividing by length
532        // gives one row's bytes; the body's input slot still
533        // holds carry_shape bytes.
534        let carry_bytes = if *save_trajectory {
535            let total = node
536                .shape
537                .size_bytes()
538                .expect("Op::Scan trajectory output must have static shape");
539            total / *length as usize
540        } else {
541            node.shape
542                .size_bytes()
543                .expect("Op::Scan carry must have static shape")
544        };
545
546        // Bcast inputs occupy body_inputs[1..1+num_bcast] and
547        // outer node.inputs[1..1+num_bcast]. They keep their
548        // natural shape (no [length, ...] prefix) and are
549        // copied into body_buf ONCE before the scan loop.
550        let mut bcast_inputs: Vec<(usize, usize, u32)> = Vec::with_capacity(*num_bcast as usize);
551        for i in 0..*num_bcast as usize {
552            let body_b_id = body_inputs[1 + i];
553            let body_b_off = body_offsets[&body_b_id];
554            let outer_b_id = node.inputs[1 + i];
555            let outer_b_off = node_offset(arena, outer_b_id);
556            let outer_b_shape = &graph.node(outer_b_id).shape;
557            let total = outer_b_shape
558                .size_bytes()
559                .expect("Op::Scan bcast must have static shape");
560            bcast_inputs.push((body_b_off, outer_b_off, total as u32));
561        }
562
563        // xs occupy body_inputs[1+num_bcast..] and node.inputs
564        // [1+num_bcast..]. Each has shape [length, *per_step];
565        // per-step bytes = total / length.
566        let mut xs_inputs: Vec<(usize, usize, u32)> = Vec::with_capacity(*num_xs as usize);
567        let xs_base = 1 + *num_bcast as usize;
568        for i in 0..*num_xs as usize {
569            let body_x_id = body_inputs[xs_base + i];
570            let body_x_off = body_offsets[&body_x_id];
571            let outer_xs_id = node.inputs[xs_base + i];
572            let outer_xs_off = node_offset(arena, outer_xs_id);
573            let outer_xs_shape = &graph.node(outer_xs_id).shape;
574            let total = outer_xs_shape
575                .size_bytes()
576                .expect("Op::Scan xs must have static shape");
577            let per_step = total / *length as usize;
578            xs_inputs.push((body_x_off, outer_xs_off, per_step as u32));
579        }
580
581        Thunk::Scan {
582            body: Arc::new(body_schedule),
583            body_init: Arc::new(body_init),
584            body_input_off,
585            body_output_off,
586            outer_init_off: node_offset(arena, node.inputs[0]),
587            outer_final_off: node_offset(arena, node.id),
588            length: *length,
589            carry_bytes: carry_bytes as u32,
590            save_trajectory: *save_trajectory,
591            xs_inputs: Arc::new(xs_inputs),
592            bcast_inputs: Arc::new(bcast_inputs),
593            num_checkpoints: *num_checkpoints,
594        }
595    }
596}
597
598#[allow(unused_variables)]
599pub(crate) fn compile_scan_backward(
600    node: &rlx_ir::Node,
601    graph: &Graph,
602    arena: &crate::arena::Arena,
603    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
604    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
605    rng: rlx_ir::RngOptions,
606) -> Thunk {
607    let Op::ScanBackward {
608        body_vjp,
609        length,
610        save_trajectory,
611        num_xs,
612        num_checkpoints,
613        forward_body,
614    } = &node.op
615    else {
616        unreachable!()
617    };
618    {
619        let is_recursive = *num_checkpoints != 0 && *num_checkpoints != *length;
620        if is_recursive {
621            assert!(
622                forward_body.is_some(),
623                "Op::ScanBackward with num_checkpoints<length requires forward_body"
624            );
625        }
626        // body_vjp has signature
627        //   (carry, x_t_0, ..., x_t_{num_xs-1}, d_output) → dcarry
628        // Identify slots:
629        //   * "d_output" by exact name (AD-introduced seed Input).
630        //   * Remaining Inputs sorted by NodeId — first is the
631        //     carry mirror, rest are x_t_i mirrors in body's
632        //     original Op::Input declaration order.
633        let body_plan = rlx_opt::memory::plan_memory(body_vjp);
634        let body_offsets: HashMap<NodeId, usize> = body_plan
635            .assignments
636            .iter()
637            .map(|(id, slot)| (*id, slot.offset))
638            .collect();
639        let mut body_d_output_off: Option<usize> = None;
640        let mut body_other_inputs: Vec<(NodeId, usize)> = Vec::new();
641        for n in body_vjp.nodes() {
642            if let Op::Input { name } = &n.op {
643                let off = body_offsets[&n.id];
644                if name == "d_output" {
645                    body_d_output_off = Some(off);
646                } else {
647                    body_other_inputs.push((n.id, off));
648                }
649            }
650        }
651        body_other_inputs.sort_by_key(|(id, _)| *id);
652        let body_d_output_off =
653            body_d_output_off.expect("ScanBackward body_vjp missing 'd_output' Input");
654        let expected_others = 1 + *num_xs as usize;
655        assert_eq!(
656            body_other_inputs.len(),
657            expected_others,
658            "ScanBackward body_vjp has {} non-d_output Inputs; \
659                     expected {} (1 carry + {} xs)",
660            body_other_inputs.len(),
661            expected_others,
662            num_xs
663        );
664        let body_carry_in_off = body_other_inputs[0].1;
665        let body_x_offs: Vec<usize> = body_other_inputs
666            .iter()
667            .skip(1)
668            .map(|(_, off)| *off)
669            .collect();
670        let body_dcarry_out_off = body_offsets[&body_vjp.outputs[0]];
671
672        let mut body_arena = crate::arena::Arena::from_plan(body_plan);
673        // Fill body_vjp's Constants (mirrors the Scan lowering).
674        for n in body_vjp.nodes() {
675            if let Op::Constant { data } = &n.op
676                && body_arena.has_buffer(n.id)
677                && !data.is_empty()
678            {
679                match n.shape.dtype() {
680                    rlx_ir::DType::F64 => {
681                        let off = body_arena.byte_offset(n.id);
682                        let buf = body_arena.raw_buf_mut();
683                        let nb = (buf.len() - off).min(data.len());
684                        buf[off..off + nb].copy_from_slice(&data[..nb]);
685                    }
686                    _ => {
687                        let buf = body_arena.slice_mut(n.id);
688                        let nf = data.len() / 4;
689                        let nl = buf.len().min(nf);
690                        for i in 0..nl {
691                            let bytes = [
692                                data[i * 4],
693                                data[i * 4 + 1],
694                                data[i * 4 + 2],
695                                data[i * 4 + 3],
696                            ];
697                            buf[i] = f32::from_le_bytes(bytes);
698                        }
699                    }
700                }
701            }
702        }
703        let body_init = body_arena.raw_buf().to_vec();
704        let body_schedule = compile_thunks_with_rng(body_vjp, &body_arena, rng);
705
706        // Carry bytes from the dcarry output node (== carry shape).
707        let carry_bytes = body_vjp
708            .node(body_vjp.outputs[0])
709            .shape
710            .size_bytes()
711            .expect("ScanBackward dcarry must be statically shaped");
712        let carry_elem_size = body_vjp
713            .node(body_vjp.outputs[0])
714            .shape
715            .dtype()
716            .size_bytes() as u32;
717
718        // For each xs input on the outer node:
719        // (outer_xs_base, per_step_bytes).
720        let mut outer_xs_offs: Vec<(usize, u32)> = Vec::with_capacity(*num_xs as usize);
721        for i in 0..*num_xs as usize {
722            let outer_xs_id = node.inputs[3 + i];
723            let outer_xs_off = node_offset(arena, outer_xs_id);
724            let outer_xs_shape = &graph.node(outer_xs_id).shape;
725            let total = outer_xs_shape
726                .size_bytes()
727                .expect("ScanBackward xs must have static shape");
728            let per_step = total / *length as usize;
729            outer_xs_offs.push((outer_xs_off, per_step as u32));
730        }
731
732        // If recursive checkpointing is active, we also compile
733        // the forward body so the executor can recompute
734        // intermediate carries. The forward body is supplied
735        // by the AD pass via `forward_body: Some(_)`.
736        let (fb_schedule, fb_init, fb_carry_in_off, fb_output_off, fb_x_offs) = if is_recursive {
737            let fb = forward_body.as_ref().unwrap();
738            let fb_plan = rlx_opt::memory::plan_memory(fb);
739            let fb_offsets: HashMap<NodeId, usize> = fb_plan
740                .assignments
741                .iter()
742                .map(|(id, slot)| (*id, slot.offset))
743                .collect();
744            let mut fb_inputs: Vec<NodeId> = fb
745                .nodes()
746                .iter()
747                .filter(|n| matches!(n.op, Op::Input { .. }))
748                .map(|n| n.id)
749                .collect();
750            fb_inputs.sort();
751            let fb_carry = fb_offsets[&fb_inputs[0]];
752            let fb_xs: Vec<usize> = (1..fb_inputs.len())
753                .map(|i| fb_offsets[&fb_inputs[i]])
754                .collect();
755            let fb_out = fb_offsets[&fb.outputs[0]];
756            let mut fb_arena = crate::arena::Arena::from_plan(fb_plan);
757            for n in fb.nodes() {
758                if let Op::Constant { data } = &n.op
759                    && fb_arena.has_buffer(n.id)
760                    && !data.is_empty()
761                {
762                    // Byte-copy works for any
763                    // numeric dtype as long as the
764                    // arena slot is sized to hold
765                    // it — the Constant's `data`
766                    // already encodes the right
767                    // bytes per element.
768                    let off = fb_arena.byte_offset(n.id);
769                    let buf = fb_arena.raw_buf_mut();
770                    let nb = (buf.len() - off).min(data.len());
771                    buf[off..off + nb].copy_from_slice(&data[..nb]);
772                }
773            }
774            let fb_init_bytes = fb_arena.raw_buf().to_vec();
775            let fb_sched = compile_thunks_with_rng(fb, &fb_arena, rng);
776            (
777                Some(Arc::new(fb_sched)),
778                Some(Arc::new(fb_init_bytes)),
779                fb_carry,
780                fb_out,
781                fb_xs,
782            )
783        } else {
784            (None, None, 0, 0, Vec::new())
785        };
786
787        Thunk::ScanBackward {
788            body_vjp: Arc::new(body_schedule),
789            body_init: Arc::new(body_init),
790            body_carry_in_off,
791            body_x_offs: Arc::new(body_x_offs),
792            body_d_output_off,
793            body_dcarry_out_off,
794            outer_init_off: node_offset(arena, node.inputs[0]),
795            outer_traj_off: node_offset(arena, node.inputs[1]),
796            outer_upstream_off: node_offset(arena, node.inputs[2]),
797            outer_xs_offs: Arc::new(outer_xs_offs),
798            outer_dinit_off: node_offset(arena, node.id),
799            length: *length,
800            carry_bytes: carry_bytes as u32,
801            carry_elem_size,
802            save_trajectory: *save_trajectory,
803            num_checkpoints: *num_checkpoints,
804            forward_body: fb_schedule,
805            forward_body_init: fb_init,
806            forward_body_carry_in_off: fb_carry_in_off,
807            forward_body_output_off: fb_output_off,
808            forward_body_x_offs: Arc::new(fb_x_offs),
809        }
810    }
811}
812
813#[allow(unused_variables)]
814pub(crate) fn compile_scan_backward_xs(
815    node: &rlx_ir::Node,
816    graph: &Graph,
817    arena: &crate::arena::Arena,
818    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
819    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
820    rng: rlx_ir::RngOptions,
821) -> Thunk {
822    let Op::ScanBackwardXs {
823        body_vjp,
824        length,
825        save_trajectory,
826        num_xs,
827        xs_idx,
828        num_checkpoints,
829        forward_body,
830    } = &node.op
831    else {
832        unreachable!()
833    };
834    {
835        assert!(
836            *num_checkpoints == 0 || *num_checkpoints <= *length,
837            "Op::ScanBackwardXs: num_checkpoints={} must be 0 or ≤ length={}",
838            *num_checkpoints,
839            *length
840        );
841        let is_recursive = *num_checkpoints != 0 && *num_checkpoints != *length;
842        if is_recursive {
843            assert!(
844                forward_body.is_some(),
845                "Op::ScanBackwardXs with num_checkpoints<length \
846                         requires forward_body"
847            );
848        }
849        // Mirror ScanBackward's body_vjp slot identification +
850        // arena prep, then add: per-iteration extraction of the
851        // body_vjp output that corresponds to the chosen xs.
852        //
853        // body_vjp's outputs (from `grad(body, [carry, xs_0, ..., xs_{num_xs-1}])`):
854        //   outputs[0]      = dcarry
855        //   outputs[1 + i]  = dx_t_i
856        let body_plan = rlx_opt::memory::plan_memory(body_vjp);
857        let body_offsets: HashMap<NodeId, usize> = body_plan
858            .assignments
859            .iter()
860            .map(|(id, slot)| (*id, slot.offset))
861            .collect();
862        let mut body_d_output_off: Option<usize> = None;
863        let mut body_other_inputs: Vec<(NodeId, usize)> = Vec::new();
864        for n in body_vjp.nodes() {
865            if let Op::Input { name } = &n.op {
866                let off = body_offsets[&n.id];
867                if name == "d_output" {
868                    body_d_output_off = Some(off);
869                } else {
870                    body_other_inputs.push((n.id, off));
871                }
872            }
873        }
874        body_other_inputs.sort_by_key(|(id, _)| *id);
875        let body_d_output_off =
876            body_d_output_off.expect("ScanBackwardXs body_vjp missing 'd_output' Input");
877        let expected_others = 1 + *num_xs as usize;
878        assert_eq!(
879            body_other_inputs.len(),
880            expected_others,
881            "ScanBackwardXs body_vjp has {} non-d_output Inputs; expected {}",
882            body_other_inputs.len(),
883            expected_others
884        );
885        let body_carry_in_off = body_other_inputs[0].1;
886        let body_x_offs: Vec<usize> = body_other_inputs
887            .iter()
888            .skip(1)
889            .map(|(_, off)| *off)
890            .collect();
891        let body_dcarry_out_off = body_offsets[&body_vjp.outputs[0]];
892        let dxs_out_node = body_vjp.outputs[1 + *xs_idx as usize];
893        let body_dxs_out_off = body_offsets[&dxs_out_node];
894
895        let mut body_arena = crate::arena::Arena::from_plan(body_plan);
896        for n in body_vjp.nodes() {
897            if let Op::Constant { data } = &n.op
898                && body_arena.has_buffer(n.id)
899                && !data.is_empty()
900            {
901                match n.shape.dtype() {
902                    rlx_ir::DType::F64 => {
903                        let off = body_arena.byte_offset(n.id);
904                        let buf = body_arena.raw_buf_mut();
905                        let nb = (buf.len() - off).min(data.len());
906                        buf[off..off + nb].copy_from_slice(&data[..nb]);
907                    }
908                    _ => {
909                        let buf = body_arena.slice_mut(n.id);
910                        let nf = data.len() / 4;
911                        let nl = buf.len().min(nf);
912                        for i in 0..nl {
913                            let bytes = [
914                                data[i * 4],
915                                data[i * 4 + 1],
916                                data[i * 4 + 2],
917                                data[i * 4 + 3],
918                            ];
919                            buf[i] = f32::from_le_bytes(bytes);
920                        }
921                    }
922                }
923            }
924        }
925        let body_init = body_arena.raw_buf().to_vec();
926        let body_schedule = compile_thunks_with_rng(body_vjp, &body_arena, rng);
927
928        let carry_bytes = body_vjp
929            .node(body_vjp.outputs[0])
930            .shape
931            .size_bytes()
932            .expect("ScanBackwardXs dcarry must be statically shaped");
933        let carry_elem_size = body_vjp
934            .node(body_vjp.outputs[0])
935            .shape
936            .dtype()
937            .size_bytes() as u32;
938        let per_step_bytes = body_vjp
939            .node(dxs_out_node)
940            .shape
941            .size_bytes()
942            .expect("ScanBackwardXs dxs body output must be statically shaped");
943
944        let mut outer_xs_offs: Vec<(usize, u32)> = Vec::with_capacity(*num_xs as usize);
945        for i in 0..*num_xs as usize {
946            let outer_xs_id = node.inputs[3 + i];
947            let outer_xs_off = node_offset(arena, outer_xs_id);
948            let outer_xs_shape = &graph.node(outer_xs_id).shape;
949            let total = outer_xs_shape
950                .size_bytes()
951                .expect("ScanBackwardXs xs must have static shape");
952            let per_step = total / *length as usize;
953            outer_xs_offs.push((outer_xs_off, per_step as u32));
954        }
955
956        // Compile forward_body for recompute when checkpointed.
957        // Mirrors the same code path in the ScanBackward arm.
958        let (fb_schedule, fb_init, fb_carry_in_off, fb_output_off, fb_x_offs) = if is_recursive {
959            let fb = forward_body.as_ref().unwrap();
960            let fb_plan = rlx_opt::memory::plan_memory(fb);
961            let fb_offsets: HashMap<NodeId, usize> = fb_plan
962                .assignments
963                .iter()
964                .map(|(id, slot)| (*id, slot.offset))
965                .collect();
966            let mut fb_inputs: Vec<NodeId> = fb
967                .nodes()
968                .iter()
969                .filter(|n| matches!(n.op, Op::Input { .. }))
970                .map(|n| n.id)
971                .collect();
972            fb_inputs.sort();
973            let fb_carry = fb_offsets[&fb_inputs[0]];
974            let fb_xs: Vec<usize> = (1..fb_inputs.len())
975                .map(|i| fb_offsets[&fb_inputs[i]])
976                .collect();
977            let fb_out = fb_offsets[&fb.outputs[0]];
978            let mut fb_arena = crate::arena::Arena::from_plan(fb_plan);
979            for n in fb.nodes() {
980                if let Op::Constant { data } = &n.op
981                    && fb_arena.has_buffer(n.id)
982                    && !data.is_empty()
983                {
984                    // Byte-copy works for any
985                    // numeric dtype as long as the
986                    // arena slot is sized to hold
987                    // it — the Constant's `data`
988                    // already encodes the right
989                    // bytes per element.
990                    let off = fb_arena.byte_offset(n.id);
991                    let buf = fb_arena.raw_buf_mut();
992                    let nb = (buf.len() - off).min(data.len());
993                    buf[off..off + nb].copy_from_slice(&data[..nb]);
994                }
995            }
996            let fb_init_bytes = fb_arena.raw_buf().to_vec();
997            let fb_sched = compile_thunks_with_rng(fb, &fb_arena, rng);
998            (
999                Some(Arc::new(fb_sched)),
1000                Some(Arc::new(fb_init_bytes)),
1001                fb_carry,
1002                fb_out,
1003                fb_xs,
1004            )
1005        } else {
1006            (None, None, 0, 0, Vec::new())
1007        };
1008
1009        Thunk::ScanBackwardXs {
1010            body_vjp: Arc::new(body_schedule),
1011            body_init: Arc::new(body_init),
1012            body_carry_in_off,
1013            body_x_offs: Arc::new(body_x_offs),
1014            body_d_output_off,
1015            body_dcarry_out_off,
1016            body_dxs_out_off,
1017            outer_init_off: node_offset(arena, node.inputs[0]),
1018            outer_traj_off: node_offset(arena, node.inputs[1]),
1019            outer_upstream_off: node_offset(arena, node.inputs[2]),
1020            outer_xs_offs: Arc::new(outer_xs_offs),
1021            outer_dxs_off: node_offset(arena, node.id),
1022            length: *length,
1023            carry_bytes: carry_bytes as u32,
1024            carry_elem_size,
1025            per_step_bytes: per_step_bytes as u32,
1026            save_trajectory: *save_trajectory,
1027            num_checkpoints: *num_checkpoints,
1028            forward_body: fb_schedule,
1029            forward_body_init: fb_init,
1030            forward_body_carry_in_off: fb_carry_in_off,
1031            forward_body_output_off: fb_output_off,
1032            forward_body_x_offs: Arc::new(fb_x_offs),
1033        }
1034    }
1035}
1036
1037#[inline(always)]
1038pub(crate) fn exec_scan(t: &Thunk, base: *mut u8) {
1039    let Thunk::Scan {
1040        body,
1041        body_init,
1042        body_input_off,
1043        body_output_off,
1044        outer_init_off,
1045        outer_final_off,
1046        length,
1047        carry_bytes,
1048        save_trajectory,
1049        xs_inputs,
1050        bcast_inputs,
1051        num_checkpoints,
1052    } = t
1053    else {
1054        unreachable!()
1055    };
1056    {
1057        let cb = *carry_bytes as usize;
1058        let n_steps = *length as usize;
1059        // Checkpoint mode: when 0 < K < length, save trajectory[k]
1060        // only when t == c_k = floor((k+1) * length / K) - 1.
1061        // The last index c_{K-1} = length - 1 always.
1062        let k_total = if *num_checkpoints == 0 || *num_checkpoints == *length {
1063            n_steps // save every step
1064        } else {
1065            *num_checkpoints as usize
1066        };
1067        let checkpoint_t_for_k = |k: usize| -> usize {
1068            if k_total == n_steps {
1069                k
1070            } else {
1071                ((k + 1) * n_steps)
1072                    .div_ceil(k_total)
1073                    .saturating_sub(1)
1074                    .min(n_steps - 1)
1075            }
1076        };
1077        let mut next_k = 0usize;
1078
1079        let mut body_buf: Vec<u8> = (**body_init).clone();
1080        unsafe {
1081            std::ptr::copy_nonoverlapping(
1082                base.add(*outer_init_off),
1083                body_buf.as_mut_ptr().add(*body_input_off),
1084                cb,
1085            );
1086            // Broadcast inputs: copy each one into the body's
1087            // input slot ONCE. They aren't touched in the
1088            // iteration loop below (in contrast to xs).
1089            for (body_b_off, outer_b_off, total_bytes) in bcast_inputs.iter() {
1090                std::ptr::copy_nonoverlapping(
1091                    base.add(*outer_b_off),
1092                    body_buf.as_mut_ptr().add(*body_b_off),
1093                    *total_bytes as usize,
1094                );
1095            }
1096        }
1097        for t in 0..n_steps {
1098            for (body_x_off, outer_xs_off, per_step_bytes) in xs_inputs.iter() {
1099                let psb = *per_step_bytes as usize;
1100                unsafe {
1101                    std::ptr::copy_nonoverlapping(
1102                        base.add(*outer_xs_off + t * psb),
1103                        body_buf.as_mut_ptr().add(*body_x_off),
1104                        psb,
1105                    );
1106                }
1107            }
1108
1109            execute_thunks(body, &mut body_buf);
1110
1111            if *save_trajectory && next_k < k_total && t == checkpoint_t_for_k(next_k) {
1112                unsafe {
1113                    std::ptr::copy_nonoverlapping(
1114                        body_buf.as_ptr().add(*body_output_off),
1115                        base.add(*outer_final_off + next_k * cb),
1116                        cb,
1117                    );
1118                }
1119                next_k += 1;
1120            }
1121
1122            if *body_output_off != *body_input_off {
1123                body_buf.copy_within(*body_output_off..*body_output_off + cb, *body_input_off);
1124            }
1125        }
1126
1127        if !*save_trajectory {
1128            // Single final-carry write.
1129            unsafe {
1130                std::ptr::copy_nonoverlapping(
1131                    body_buf.as_ptr().add(*body_output_off),
1132                    base.add(*outer_final_off),
1133                    cb,
1134                );
1135            }
1136        }
1137    }
1138}
1139
1140#[inline(always)]
1141pub(crate) fn exec_scan_backward_xs(t: &Thunk, base: *mut u8) {
1142    let Thunk::ScanBackwardXs {
1143        body_vjp,
1144        body_init,
1145        body_carry_in_off,
1146        body_x_offs,
1147        body_d_output_off,
1148        body_dcarry_out_off,
1149        body_dxs_out_off,
1150        outer_init_off,
1151        outer_traj_off,
1152        outer_upstream_off,
1153        outer_xs_offs,
1154        outer_dxs_off,
1155        length,
1156        carry_bytes,
1157        carry_elem_size,
1158        per_step_bytes,
1159        save_trajectory,
1160        num_checkpoints,
1161        forward_body,
1162        forward_body_init,
1163        forward_body_carry_in_off,
1164        forward_body_output_off,
1165        forward_body_x_offs,
1166    } = t
1167    else {
1168        unreachable!()
1169    };
1170    {
1171        let cb = *carry_bytes as usize;
1172        let psb = *per_step_bytes as usize;
1173        let n_steps = *length as usize;
1174        let k_total = *num_checkpoints as usize;
1175        let is_recursive = k_total != 0 && k_total != n_steps;
1176        let checkpoint_t_for_k = |k: usize| -> usize {
1177            ((k + 1) * n_steps)
1178                .div_ceil(k_total)
1179                .saturating_sub(1)
1180                .min(n_steps - 1)
1181        };
1182
1183        // Forward-body recompute scratch + segment cache —
1184        // exact mirror of the ScanBackward path. With ≈√length
1185        // checkpoints, total recompute work is O(length).
1186        let mut fwd_buf: Vec<u8> = if is_recursive {
1187            (**forward_body_init.as_ref().unwrap()).clone()
1188        } else {
1189            Vec::new()
1190        };
1191        let mut seg_cache: Vec<u8> = Vec::new();
1192        let mut seg_start_t: usize = usize::MAX;
1193        let mut seg_count: usize = 0;
1194        let recompute_carry_t = |t: usize,
1195                                 dst: &mut [u8],
1196                                 fwd_buf: &mut Vec<u8>,
1197                                 seg_cache: &mut Vec<u8>,
1198                                 seg_start_t: &mut usize,
1199                                 seg_count: &mut usize| {
1200            if !is_recursive {
1201                unsafe {
1202                    let src = if t == 0 {
1203                        base.add(*outer_init_off)
1204                    } else {
1205                        base.add(*outer_traj_off + (t - 1) * cb)
1206                    };
1207                    std::ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), cb);
1208                }
1209                return;
1210            }
1211            if *seg_start_t != usize::MAX && t >= *seg_start_t && t < *seg_start_t + *seg_count {
1212                let off = (t - *seg_start_t) * cb;
1213                dst.copy_from_slice(&seg_cache[off..off + cb]);
1214                return;
1215            }
1216            let seg_k = (0..k_total)
1217                .find(|&k| t <= checkpoint_t_for_k(k))
1218                .unwrap_or(k_total - 1);
1219            let (anchor_t, anchor_ptr): (usize, *const u8) = if seg_k == 0 {
1220                (0, unsafe { base.add(*outer_init_off) as *const u8 })
1221            } else {
1222                let prev_ck = checkpoint_t_for_k(seg_k - 1);
1223                (prev_ck + 1, unsafe {
1224                    base.add(*outer_traj_off + (seg_k - 1) * cb) as *const u8
1225                })
1226            };
1227            let seg_end_t = checkpoint_t_for_k(seg_k);
1228            let seg_size = seg_end_t - anchor_t + 1;
1229
1230            fwd_buf.copy_from_slice(forward_body_init.as_ref().unwrap());
1231            unsafe {
1232                std::ptr::copy_nonoverlapping(
1233                    anchor_ptr,
1234                    fwd_buf.as_mut_ptr().add(*forward_body_carry_in_off),
1235                    cb,
1236                );
1237            }
1238            seg_cache.resize(seg_size * cb, 0u8);
1239            seg_cache[0..cb].copy_from_slice(
1240                &fwd_buf[*forward_body_carry_in_off..*forward_body_carry_in_off + cb],
1241            );
1242            let fb_sched = forward_body.as_ref().unwrap();
1243            for i in 1..seg_size {
1244                let cur_iter = anchor_t + i - 1;
1245                for (idx, fb_x_off) in forward_body_x_offs.iter().enumerate() {
1246                    let (outer_xs_off, x_psb) = outer_xs_offs[idx];
1247                    let xb = x_psb as usize;
1248                    unsafe {
1249                        std::ptr::copy_nonoverlapping(
1250                            base.add(outer_xs_off + cur_iter * xb),
1251                            fwd_buf.as_mut_ptr().add(*fb_x_off),
1252                            xb,
1253                        );
1254                    }
1255                }
1256                execute_thunks(fb_sched, fwd_buf);
1257                if *forward_body_output_off != *forward_body_carry_in_off {
1258                    fwd_buf.copy_within(
1259                        *forward_body_output_off..*forward_body_output_off + cb,
1260                        *forward_body_carry_in_off,
1261                    );
1262                }
1263                let cache_off = i * cb;
1264                seg_cache[cache_off..cache_off + cb].copy_from_slice(
1265                    &fwd_buf[*forward_body_carry_in_off..*forward_body_carry_in_off + cb],
1266                );
1267            }
1268            *seg_start_t = anchor_t;
1269            *seg_count = seg_size;
1270
1271            let off = (t - anchor_t) * cb;
1272            dst.copy_from_slice(&seg_cache[off..off + cb]);
1273        };
1274
1275        let mut dcarry: Vec<u8> = vec![0u8; cb];
1276        if !*save_trajectory {
1277            unsafe {
1278                std::ptr::copy_nonoverlapping(
1279                    base.add(*outer_upstream_off),
1280                    dcarry.as_mut_ptr(),
1281                    cb,
1282                );
1283            }
1284        }
1285
1286        let mut body_buf: Vec<u8> = (**body_init).clone();
1287
1288        for t in (0..n_steps).rev() {
1289            if *save_trajectory {
1290                unsafe {
1291                    let up_off = *outer_upstream_off + t * cb;
1292                    match *carry_elem_size {
1293                        4 => {
1294                            let up_ptr = base.add(up_off) as *const f32;
1295                            let dc_ptr = dcarry.as_mut_ptr() as *mut f32;
1296                            let n_elems = cb / 4;
1297                            for i in 0..n_elems {
1298                                *dc_ptr.add(i) += *up_ptr.add(i);
1299                            }
1300                        }
1301                        8 => {
1302                            let up_ptr = base.add(up_off) as *const f64;
1303                            let dc_ptr = dcarry.as_mut_ptr() as *mut f64;
1304                            let n_elems = cb / 8;
1305                            for i in 0..n_elems {
1306                                *dc_ptr.add(i) += *up_ptr.add(i);
1307                            }
1308                        }
1309                        other => panic!(
1310                            "ScanBackwardXs: unsupported carry elem size {other} \
1311                                     (only f32/f64 carries are supported today)"
1312                        ),
1313                    }
1314                }
1315            }
1316
1317            // Seed body_vjp's carry input via the recompute
1318            // helper (works for both All and Recursive modes),
1319            // then x_t_i + d_output.
1320            let carry_dst_start = *body_carry_in_off;
1321            {
1322                let carry_slice = &mut body_buf[carry_dst_start..carry_dst_start + cb];
1323                recompute_carry_t(
1324                    t,
1325                    carry_slice,
1326                    &mut fwd_buf,
1327                    &mut seg_cache,
1328                    &mut seg_start_t,
1329                    &mut seg_count,
1330                );
1331            }
1332            unsafe {
1333                for (i, body_x_off) in body_x_offs.iter().enumerate() {
1334                    let (outer_xs_off, x_psb) = outer_xs_offs[i];
1335                    let xb = x_psb as usize;
1336                    std::ptr::copy_nonoverlapping(
1337                        base.add(outer_xs_off + t * xb),
1338                        body_buf.as_mut_ptr().add(*body_x_off),
1339                        xb,
1340                    );
1341                }
1342                std::ptr::copy_nonoverlapping(
1343                    dcarry.as_ptr(),
1344                    body_buf.as_mut_ptr().add(*body_d_output_off),
1345                    cb,
1346                );
1347            }
1348
1349            execute_thunks(body_vjp, &mut body_buf);
1350
1351            // Stash this step's dxs into row `t` of the outer
1352            // [length, *per_step_xs] output.
1353            unsafe {
1354                std::ptr::copy_nonoverlapping(
1355                    body_buf.as_ptr().add(*body_dxs_out_off),
1356                    base.add(*outer_dxs_off + t * psb),
1357                    psb,
1358                );
1359            }
1360
1361            // Update dcarry for next backward iteration.
1362            unsafe {
1363                std::ptr::copy_nonoverlapping(
1364                    body_buf.as_ptr().add(*body_dcarry_out_off),
1365                    dcarry.as_mut_ptr(),
1366                    cb,
1367                );
1368            }
1369        }
1370    }
1371}
1372
1373#[inline(always)]
1374pub(crate) fn exec_gated_delta_net(t: &Thunk, base: *mut u8) {
1375    let Thunk::GatedDeltaNet {
1376        q,
1377        k,
1378        v,
1379        g,
1380        beta,
1381        state,
1382        dst,
1383        batch,
1384        seq,
1385        heads,
1386        state_size,
1387    } = t
1388    else {
1389        unreachable!()
1390    };
1391    unsafe {
1392        execute_gated_delta_net_f32(
1393            *q,
1394            *k,
1395            *v,
1396            *g,
1397            *beta,
1398            *state,
1399            *dst,
1400            *batch as usize,
1401            *seq as usize,
1402            *heads as usize,
1403            *state_size as usize,
1404            base,
1405        );
1406    }
1407}
1408
1409#[inline(always)]
1410pub(crate) fn exec_lstm(t: &Thunk, base: *mut u8) {
1411    let Thunk::Lstm {
1412        x,
1413        w_ih,
1414        w_hh,
1415        bias,
1416        h0,
1417        c0,
1418        dst,
1419        batch,
1420        seq,
1421        input_size,
1422        hidden,
1423        num_layers,
1424        bidirectional,
1425        carry,
1426    } = t
1427    else {
1428        unreachable!()
1429    };
1430    unsafe {
1431        execute_lstm_f32(
1432            *x,
1433            *w_ih,
1434            *w_hh,
1435            *bias,
1436            *h0,
1437            *c0,
1438            *dst,
1439            *batch as usize,
1440            *seq as usize,
1441            *input_size as usize,
1442            *hidden as usize,
1443            *num_layers as usize,
1444            *bidirectional,
1445            *carry,
1446            base,
1447        );
1448    }
1449}
1450
1451#[inline(always)]
1452pub(crate) fn exec_gru(t: &Thunk, base: *mut u8) {
1453    let Thunk::Gru {
1454        x,
1455        w_ih,
1456        w_hh,
1457        b_ih,
1458        b_hh,
1459        h0,
1460        dst,
1461        batch,
1462        seq,
1463        input_size,
1464        hidden,
1465        num_layers,
1466        bidirectional,
1467        carry,
1468    } = t
1469    else {
1470        unreachable!()
1471    };
1472    unsafe {
1473        execute_gru_f32(
1474            *x,
1475            *w_ih,
1476            *w_hh,
1477            *b_ih,
1478            *b_hh,
1479            *h0,
1480            *dst,
1481            *batch as usize,
1482            *seq as usize,
1483            *input_size as usize,
1484            *hidden as usize,
1485            *num_layers as usize,
1486            *bidirectional,
1487            *carry,
1488            base,
1489        );
1490    }
1491}
1492
1493#[inline(always)]
1494pub(crate) fn exec_rnn(t: &Thunk, base: *mut u8) {
1495    let Thunk::Rnn {
1496        x,
1497        w_ih,
1498        w_hh,
1499        bias,
1500        h0,
1501        dst,
1502        batch,
1503        seq,
1504        input_size,
1505        hidden,
1506        num_layers,
1507        bidirectional,
1508        carry,
1509        relu,
1510    } = t
1511    else {
1512        unreachable!()
1513    };
1514    unsafe {
1515        execute_rnn_f32(
1516            *x,
1517            *w_ih,
1518            *w_hh,
1519            *bias,
1520            *h0,
1521            *dst,
1522            *batch as usize,
1523            *seq as usize,
1524            *input_size as usize,
1525            *hidden as usize,
1526            *num_layers as usize,
1527            *bidirectional,
1528            *carry,
1529            *relu,
1530            base,
1531        );
1532    }
1533}
1534
1535#[inline(always)]
1536pub(crate) fn exec_mamba2(t: &Thunk, base: *mut u8) {
1537    let Thunk::Mamba2 {
1538        x,
1539        dt,
1540        a,
1541        b,
1542        c,
1543        dst,
1544        batch,
1545        seq,
1546        heads,
1547        head_dim,
1548        state_size,
1549    } = t
1550    else {
1551        unreachable!()
1552    };
1553    unsafe {
1554        execute_mamba2_f32(
1555            *x,
1556            *dt,
1557            *a,
1558            *b,
1559            *c,
1560            *dst,
1561            *batch as usize,
1562            *seq as usize,
1563            *heads as usize,
1564            *head_dim as usize,
1565            *state_size as usize,
1566            base,
1567        );
1568    }
1569}
1570
1571#[inline(always)]
1572pub(crate) fn exec_selective_scan(t: &Thunk, base: *mut u8) {
1573    let Thunk::SelectiveScan {
1574        x,
1575        delta,
1576        a,
1577        b: bp,
1578        c: cp,
1579        dst,
1580        batch,
1581        seq,
1582        hidden,
1583        state_size,
1584    } = t
1585    else {
1586        unreachable!()
1587    };
1588    unsafe {
1589        execute_selective_scan_f32(
1590            *x,
1591            *delta,
1592            *a,
1593            *bp,
1594            *cp,
1595            *dst,
1596            *batch as usize,
1597            *seq as usize,
1598            *hidden as usize,
1599            *state_size as usize,
1600            base,
1601        );
1602    }
1603}
1604
1605/// Griewank treeverse: process backward iterations `[t_lo..=t_hi]` (with
1606/// the carry entering iteration `t_lo` supplied as `anchor_carry`) by
1607/// recursive binary subdivision. Total work `O((t_hi-t_lo+1) · log)`,
1608/// auxiliary memory `O(log · carry_bytes)` for the recursion stack.
1609///
1610/// Compared to the iterative segment-cached scheme, this trades extra
1611/// recompute for less working memory — each level of recursion holds
1612/// one `cb`-sized intermediate carry on the stack but never the whole
1613/// segment at once. With K saved outer checkpoints, the outer driver
1614/// invokes this helper once per segment.
1615///
1616/// `process_iter(t, carry_at_t)` is the per-iteration leaf action: it
1617/// runs `body_vjp` at iteration `t` with the supplied carry, threads
1618/// `dcarry` backward, and (for ScanBackwardXs) writes `dxs[t]`.
1619#[allow(clippy::too_many_arguments)]
1620pub(crate) unsafe fn griewank_process_segment(
1621    t_lo: usize,
1622    t_hi: usize,
1623    anchor_carry: &[u8],
1624    cb: usize,
1625    fwd_sched: &ThunkSchedule,
1626    fwd_init: &[u8],
1627    fwd_carry_in_off: usize,
1628    fwd_output_off: usize,
1629    fwd_x_offs: &[usize],
1630    base: *mut u8,
1631    outer_xs_offs: &[(usize, u32)],
1632    fwd_buf: &mut Vec<u8>,
1633    leaf_threshold: usize,
1634    process_iter: &mut dyn FnMut(usize, &[u8]),
1635) {
1636    unsafe {
1637        let size = t_hi - t_lo + 1;
1638        if size == 1 {
1639            process_iter(t_lo, anchor_carry);
1640            return;
1641        }
1642        if size <= leaf_threshold {
1643            // Walk forward, cache each carry, run backward in reverse.
1644            let mut cache: Vec<u8> = Vec::with_capacity(size * cb);
1645            cache.extend_from_slice(anchor_carry);
1646            fwd_buf.copy_from_slice(fwd_init);
1647            std::ptr::copy_nonoverlapping(
1648                anchor_carry.as_ptr(),
1649                fwd_buf.as_mut_ptr().add(fwd_carry_in_off),
1650                cb,
1651            );
1652            for i in 1..size {
1653                let cur_iter = t_lo + i - 1;
1654                for (idx, fb_x_off) in fwd_x_offs.iter().enumerate() {
1655                    let (outer_xs_off, x_psb) = outer_xs_offs[idx];
1656                    let xb = x_psb as usize;
1657                    std::ptr::copy_nonoverlapping(
1658                        base.add(outer_xs_off + cur_iter * xb),
1659                        fwd_buf.as_mut_ptr().add(*fb_x_off),
1660                        xb,
1661                    );
1662                }
1663                execute_thunks(fwd_sched, fwd_buf);
1664                if fwd_output_off != fwd_carry_in_off {
1665                    fwd_buf.copy_within(fwd_output_off..fwd_output_off + cb, fwd_carry_in_off);
1666                }
1667                cache.extend_from_slice(&fwd_buf[fwd_carry_in_off..fwd_carry_in_off + cb]);
1668            }
1669            // Process backward.
1670            for t in (t_lo..=t_hi).rev() {
1671                let idx = t - t_lo;
1672                let carry = &cache[idx * cb..(idx + 1) * cb];
1673                process_iter(t, carry);
1674            }
1675            return;
1676        }
1677
1678        // Split: walk forward from anchor to compute carry entering `mid`.
1679        // (We need `mid - t_lo` body executions: one per iteration in
1680        // [t_lo, mid).)
1681        let mid = t_lo + size / 2;
1682        fwd_buf.copy_from_slice(fwd_init);
1683        std::ptr::copy_nonoverlapping(
1684            anchor_carry.as_ptr(),
1685            fwd_buf.as_mut_ptr().add(fwd_carry_in_off),
1686            cb,
1687        );
1688        for cur_iter in t_lo..mid {
1689            for (idx, fb_x_off) in fwd_x_offs.iter().enumerate() {
1690                let (outer_xs_off, x_psb) = outer_xs_offs[idx];
1691                let xb = x_psb as usize;
1692                std::ptr::copy_nonoverlapping(
1693                    base.add(outer_xs_off + cur_iter * xb),
1694                    fwd_buf.as_mut_ptr().add(*fb_x_off),
1695                    xb,
1696                );
1697            }
1698            execute_thunks(fwd_sched, fwd_buf);
1699            if fwd_output_off != fwd_carry_in_off {
1700                fwd_buf.copy_within(fwd_output_off..fwd_output_off + cb, fwd_carry_in_off);
1701            }
1702        }
1703        let mid_carry: Vec<u8> = fwd_buf[fwd_carry_in_off..fwd_carry_in_off + cb].to_vec();
1704
1705        // Right half first (higher t values processed first to match the
1706        // canonical reverse-mode iteration order: dcarry threads from
1707        // t=length-1 down to t=0).
1708        griewank_process_segment(
1709            mid,
1710            t_hi,
1711            &mid_carry,
1712            cb,
1713            fwd_sched,
1714            fwd_init,
1715            fwd_carry_in_off,
1716            fwd_output_off,
1717            fwd_x_offs,
1718            base,
1719            outer_xs_offs,
1720            fwd_buf,
1721            leaf_threshold,
1722            process_iter,
1723        );
1724        // Then left half with original anchor.
1725        griewank_process_segment(
1726            t_lo,
1727            mid - 1,
1728            anchor_carry,
1729            cb,
1730            fwd_sched,
1731            fwd_init,
1732            fwd_carry_in_off,
1733            fwd_output_off,
1734            fwd_x_offs,
1735            base,
1736            outer_xs_offs,
1737            fwd_buf,
1738            leaf_threshold,
1739            process_iter,
1740        );
1741    }
1742}
1743
1744/// Expected packed-buffer element counts for the RNN-family ops, derived
1745/// purely from the op parameters. `gates` = 4 (LSTM), 3 (GRU), 1 (RNN).
1746///
1747/// The `execute_{lstm,gru,rnn}_f32` kernels read their weights through raw
1748/// pointers sized from these same parameters, so a caller that supplies a
1749/// mis-shaped buffer (e.g. a `w_hh` missing the `×hidden` factor) reads out of
1750/// bounds and returns silent garbage instead of erroring. Callers should
1751/// validate their buffers against this up front — see [`check_rnn_lens`].
1752#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1753pub struct RnnLens {
1754    /// `x`: `batch·seq·input_size`.
1755    pub x: usize,
1756    /// `w_ih`: Σ over layers of `dirs·gates·hidden·in_l(l)` (`in_l` grows to `dirs·hidden`).
1757    pub w_ih: usize,
1758    /// `w_hh`: `layers·dirs·gates·hidden·hidden`.
1759    pub w_hh: usize,
1760    /// One bias stream: `layers·dirs·gates·hidden` (LSTM/RNN merge into one; GRU has `b_ih` and `b_hh`).
1761    pub bias: usize,
1762    /// Per-state carry buffer (`h0` and, for LSTM, `c0`): `layers·dirs·batch·hidden`.
1763    pub state: usize,
1764    /// Output `dst`: `batch·seq·dirs·hidden`.
1765    pub out: usize,
1766}
1767
1768/// Expected buffer sizes for an RNN-family op with the given parameters.
1769pub fn rnn_expected_lens(
1770    gates: usize,
1771    batch: usize,
1772    seq: usize,
1773    input_size: usize,
1774    hidden: usize,
1775    num_layers: usize,
1776    bidirectional: bool,
1777) -> RnnLens {
1778    let dirs = if bidirectional { 2 } else { 1 };
1779    let w_ih: usize = (0..num_layers)
1780        .map(|l| dirs * gates * hidden * if l == 0 { input_size } else { dirs * hidden })
1781        .sum();
1782    RnnLens {
1783        x: batch * seq * input_size,
1784        w_ih,
1785        w_hh: num_layers * dirs * gates * hidden * hidden,
1786        bias: num_layers * dirs * gates * hidden,
1787        state: num_layers * dirs * batch * hidden,
1788        out: batch * seq * dirs * hidden,
1789    }
1790}
1791
1792/// Validate a labeled `(actual, expected)` list, returning a clear error on the
1793/// first mismatch. Turns an out-of-bounds kernel read into a loud failure.
1794pub fn check_rnn_lens(op: &str, checks: &[(&str, usize, usize)]) -> Result<(), String> {
1795    for &(name, actual, expected) in checks {
1796        if actual != expected {
1797            return Err(format!(
1798                "{op}: input '{name}' has {actual} elements, expected {expected} \
1799                 (mismatch would read out of bounds in the f32 kernel)"
1800            ));
1801        }
1802    }
1803    Ok(())
1804}
1805
1806/// Reference / host-fallback entry for `Op::Lstm` (multi-layer,
1807/// optionally bidirectional, optional decode carry). Gate order i, f, g,
1808/// o. Shared by the CPU backend and the CUDA / ROCm / wgpu / Metal host
1809/// fallbacks. Tensors are `f32` in the arena; weights are packed (see
1810/// `Op::Lstm`). `h0`/`c0` are byte offsets when `carry`, else ignored;
1811/// the final `hn`/`cn` are written back into them in place. `dst` is
1812/// `[batch, seq, D*hidden]`. Batch items run in parallel per direction.
1813#[allow(clippy::too_many_arguments)]
1814pub unsafe fn execute_lstm_f32(
1815    x: usize,
1816    w_ih: usize,
1817    w_hh: usize,
1818    bias: usize,
1819    h0: usize,
1820    c0: usize,
1821    dst: usize,
1822    batch: usize,
1823    seq: usize,
1824    input_size: usize,
1825    hidden: usize,
1826    num_layers: usize,
1827    bidirectional: bool,
1828    carry: bool,
1829    base: *mut u8,
1830) {
1831    #[inline]
1832    fn sigmoid(z: f32) -> f32 {
1833        1.0 / (1.0 + (-z).exp())
1834    }
1835
1836    let bptr = base as usize;
1837    let four_h = 4 * hidden;
1838    let dirs = if bidirectional { 2 } else { 1 };
1839
1840    unsafe {
1841        let f32s = |off: usize, n: usize| -> &[f32] {
1842            std::slice::from_raw_parts((bptr + off) as *const f32, n)
1843        };
1844
1845        // Layer 0 reads x; later layers read the previous layer's output.
1846        let mut layer_in: Vec<f32> = f32s(x, batch * seq * input_size).to_vec();
1847        let mut in_l = input_size;
1848        // Running element cursor into the packed `w_ih` buffer (block width
1849        // `4h * in_l` varies per layer; `w_hh`/`bias` blocks are uniform).
1850        let mut wih_cursor = 0usize;
1851
1852        for l in 0..num_layers {
1853            let out_width = dirs * hidden;
1854            let mut layer_out = vec![0f32; batch * seq * out_width];
1855            let lo_ptr = layer_out.as_mut_ptr() as usize;
1856            let li_ref: &[f32] = &layer_in;
1857            let wih_block = four_h * in_l;
1858
1859            for dir in 0..dirs {
1860                let ld = l * dirs + dir;
1861                let wih = f32s((w_ih / 4 + wih_cursor + dir * wih_block) * 4, wih_block);
1862                let whh = f32s(w_hh + ld * four_h * hidden * 4, four_h * hidden);
1863                let bs = f32s(bias + ld * four_h * 4, four_h);
1864                let h0p = bptr + h0 + ld * batch * hidden * 4;
1865                let c0p = bptr + c0 + ld * batch * hidden * 4;
1866
1867                crate::pool::par_range(batch, |b| {
1868                    let lo = lo_ptr as *mut f32;
1869                    let mut h = vec![0f32; hidden];
1870                    let mut c = vec![0f32; hidden];
1871                    if carry {
1872                        let hin = std::slice::from_raw_parts(
1873                            (h0p + b * hidden * 4) as *const f32,
1874                            hidden,
1875                        );
1876                        let cin = std::slice::from_raw_parts(
1877                            (c0p + b * hidden * 4) as *const f32,
1878                            hidden,
1879                        );
1880                        h.copy_from_slice(hin);
1881                        c.copy_from_slice(cin);
1882                    }
1883                    let mut z = vec![0f32; four_h];
1884                    for step in 0..seq {
1885                        let t = if dir == 0 { step } else { seq - 1 - step };
1886                        let x_t = &li_ref[(b * seq + t) * in_l..(b * seq + t + 1) * in_l];
1887                        for r in 0..four_h {
1888                            let wr = &wih[r * in_l..(r + 1) * in_l];
1889                            let mut acc = bs[r];
1890                            for j in 0..in_l {
1891                                acc += wr[j] * x_t[j];
1892                            }
1893                            let hr = &whh[r * hidden..(r + 1) * hidden];
1894                            for (j, &hj) in h.iter().enumerate() {
1895                                acc += hr[j] * hj;
1896                            }
1897                            z[r] = acc;
1898                        }
1899                        for k in 0..hidden {
1900                            let i_g = sigmoid(z[k]);
1901                            let f_g = sigmoid(z[hidden + k]);
1902                            let g_g = z[2 * hidden + k].tanh();
1903                            let o_g = sigmoid(z[3 * hidden + k]);
1904                            let c_new = f_g * c[k] + i_g * g_g;
1905                            c[k] = c_new;
1906                            let h_new = o_g * c_new.tanh();
1907                            h[k] = h_new;
1908                            // [batch, seq, D*hidden]; this direction owns the
1909                            // `dir*hidden .. dir*hidden+hidden` feature slice.
1910                            *lo.add((b * seq + t) * out_width + dir * hidden + k) = h_new;
1911                        }
1912                    }
1913                    if carry {
1914                        let hout = std::slice::from_raw_parts_mut(
1915                            (h0p + b * hidden * 4) as *mut f32,
1916                            hidden,
1917                        );
1918                        let cout = std::slice::from_raw_parts_mut(
1919                            (c0p + b * hidden * 4) as *mut f32,
1920                            hidden,
1921                        );
1922                        hout.copy_from_slice(&h);
1923                        cout.copy_from_slice(&c);
1924                    }
1925                });
1926            }
1927
1928            wih_cursor += dirs * wih_block;
1929            layer_in = layer_out;
1930            in_l = out_width;
1931        }
1932
1933        // Final layer output → dst [batch, seq, D*hidden].
1934        let dst_slice = std::slice::from_raw_parts_mut((bptr + dst) as *mut f32, layer_in.len());
1935        dst_slice.copy_from_slice(&layer_in);
1936    }
1937}
1938
1939/// Reference / host-fallback for `Op::Gru` (PyTorch GRU; gate order r, z, n;
1940/// multi-layer / bidirectional / carry). Separate `b_ih`/`b_hh` because the
1941/// reset gate is applied to the hidden term *after* its bias:
1942/// `n = tanh(x·W_inᵀ+b_in + r ⊙ (h·W_hnᵀ+b_hn))`,
1943/// `h' = (1-z)⊙n + z⊙h`. Packing mirrors `Op::Lstm` with `3*hidden` gate rows.
1944#[allow(clippy::too_many_arguments)]
1945pub unsafe fn execute_gru_f32(
1946    x: usize,
1947    w_ih: usize,
1948    w_hh: usize,
1949    b_ih: usize,
1950    b_hh: usize,
1951    h0: usize,
1952    dst: usize,
1953    batch: usize,
1954    seq: usize,
1955    input_size: usize,
1956    hidden: usize,
1957    num_layers: usize,
1958    bidirectional: bool,
1959    carry: bool,
1960    base: *mut u8,
1961) {
1962    #[inline]
1963    fn sigmoid(z: f32) -> f32 {
1964        1.0 / (1.0 + (-z).exp())
1965    }
1966
1967    let bptr = base as usize;
1968    let three_h = 3 * hidden;
1969    let dirs = if bidirectional { 2 } else { 1 };
1970
1971    unsafe {
1972        let f32s = |off: usize, n: usize| -> &[f32] {
1973            std::slice::from_raw_parts((bptr + off) as *const f32, n)
1974        };
1975
1976        let mut layer_in: Vec<f32> = f32s(x, batch * seq * input_size).to_vec();
1977        let mut in_l = input_size;
1978        let mut wih_cursor = 0usize;
1979
1980        for l in 0..num_layers {
1981            let out_width = dirs * hidden;
1982            let mut layer_out = vec![0f32; batch * seq * out_width];
1983            let lo_ptr = layer_out.as_mut_ptr() as usize;
1984            let li_ref: &[f32] = &layer_in;
1985            let wih_block = three_h * in_l;
1986
1987            for dir in 0..dirs {
1988                let ld = l * dirs + dir;
1989                let wih = f32s((w_ih / 4 + wih_cursor + dir * wih_block) * 4, wih_block);
1990                let whh = f32s(w_hh + ld * three_h * hidden * 4, three_h * hidden);
1991                let bih = f32s(b_ih + ld * three_h * 4, three_h);
1992                let bhh = f32s(b_hh + ld * three_h * 4, three_h);
1993                let h0p = bptr + h0 + ld * batch * hidden * 4;
1994
1995                crate::pool::par_range(batch, |b| {
1996                    let lo = lo_ptr as *mut f32;
1997                    let mut h = vec![0f32; hidden];
1998                    if carry {
1999                        let hin = std::slice::from_raw_parts(
2000                            (h0p + b * hidden * 4) as *const f32,
2001                            hidden,
2002                        );
2003                        h.copy_from_slice(hin);
2004                    }
2005                    let mut xi = vec![0f32; three_h]; // x·W_ihᵀ + b_ih
2006                    let mut hi = vec![0f32; three_h]; // h·W_hhᵀ + b_hh
2007                    for step in 0..seq {
2008                        let t = if dir == 0 { step } else { seq - 1 - step };
2009                        let x_t = &li_ref[(b * seq + t) * in_l..(b * seq + t + 1) * in_l];
2010                        for r in 0..three_h {
2011                            let wr = &wih[r * in_l..(r + 1) * in_l];
2012                            let mut a = bih[r];
2013                            for j in 0..in_l {
2014                                a += wr[j] * x_t[j];
2015                            }
2016                            xi[r] = a;
2017                            let hr = &whh[r * hidden..(r + 1) * hidden];
2018                            let mut bb = bhh[r];
2019                            for (j, &hj) in h.iter().enumerate() {
2020                                bb += hr[j] * hj;
2021                            }
2022                            hi[r] = bb;
2023                        }
2024                        for k in 0..hidden {
2025                            let rg = sigmoid(xi[k] + hi[k]);
2026                            let zg = sigmoid(xi[hidden + k] + hi[hidden + k]);
2027                            // Reset applied to hidden term after its bias.
2028                            let ng = (xi[2 * hidden + k] + rg * hi[2 * hidden + k]).tanh();
2029                            let h_new = (1.0 - zg) * ng + zg * h[k];
2030                            h[k] = h_new;
2031                            *lo.add((b * seq + t) * out_width + dir * hidden + k) = h_new;
2032                        }
2033                    }
2034                    if carry {
2035                        let hout = std::slice::from_raw_parts_mut(
2036                            (h0p + b * hidden * 4) as *mut f32,
2037                            hidden,
2038                        );
2039                        hout.copy_from_slice(&h);
2040                    }
2041                });
2042            }
2043
2044            wih_cursor += dirs * wih_block;
2045            layer_in = layer_out;
2046            in_l = out_width;
2047        }
2048
2049        let dst_slice = std::slice::from_raw_parts_mut((bptr + dst) as *mut f32, layer_in.len());
2050        dst_slice.copy_from_slice(&layer_in);
2051    }
2052}
2053
2054/// Reference / host-fallback for `Op::Rnn` (Elman; `act = relu` when `relu`
2055/// else `tanh`; multi-layer / bidirectional / carry). Single merged bias.
2056/// `h' = act(x·W_ihᵀ + h·W_hhᵀ + bias)`. Packing mirrors `Op::Lstm` with
2057/// `hidden` gate rows.
2058#[allow(clippy::too_many_arguments)]
2059pub unsafe fn execute_rnn_f32(
2060    x: usize,
2061    w_ih: usize,
2062    w_hh: usize,
2063    bias: usize,
2064    h0: usize,
2065    dst: usize,
2066    batch: usize,
2067    seq: usize,
2068    input_size: usize,
2069    hidden: usize,
2070    num_layers: usize,
2071    bidirectional: bool,
2072    carry: bool,
2073    relu: bool,
2074    base: *mut u8,
2075) {
2076    let bptr = base as usize;
2077    let dirs = if bidirectional { 2 } else { 1 };
2078
2079    unsafe {
2080        let f32s = |off: usize, n: usize| -> &[f32] {
2081            std::slice::from_raw_parts((bptr + off) as *const f32, n)
2082        };
2083
2084        let mut layer_in: Vec<f32> = f32s(x, batch * seq * input_size).to_vec();
2085        let mut in_l = input_size;
2086        let mut wih_cursor = 0usize;
2087
2088        for l in 0..num_layers {
2089            let out_width = dirs * hidden;
2090            let mut layer_out = vec![0f32; batch * seq * out_width];
2091            let lo_ptr = layer_out.as_mut_ptr() as usize;
2092            let li_ref: &[f32] = &layer_in;
2093            let wih_block = hidden * in_l;
2094
2095            for dir in 0..dirs {
2096                let ld = l * dirs + dir;
2097                let wih = f32s((w_ih / 4 + wih_cursor + dir * wih_block) * 4, wih_block);
2098                let whh = f32s(w_hh + ld * hidden * hidden * 4, hidden * hidden);
2099                let bs = f32s(bias + ld * hidden * 4, hidden);
2100                let h0p = bptr + h0 + ld * batch * hidden * 4;
2101
2102                crate::pool::par_range(batch, |b| {
2103                    let lo = lo_ptr as *mut f32;
2104                    let mut h = vec![0f32; hidden];
2105                    if carry {
2106                        let hin = std::slice::from_raw_parts(
2107                            (h0p + b * hidden * 4) as *const f32,
2108                            hidden,
2109                        );
2110                        h.copy_from_slice(hin);
2111                    }
2112                    for step in 0..seq {
2113                        let t = if dir == 0 { step } else { seq - 1 - step };
2114                        let x_t = &li_ref[(b * seq + t) * in_l..(b * seq + t + 1) * in_l];
2115                        let mut h_new = vec![0f32; hidden];
2116                        for k in 0..hidden {
2117                            let wr = &wih[k * in_l..(k + 1) * in_l];
2118                            let mut acc = bs[k];
2119                            for j in 0..in_l {
2120                                acc += wr[j] * x_t[j];
2121                            }
2122                            let hr = &whh[k * hidden..(k + 1) * hidden];
2123                            for (j, &hj) in h.iter().enumerate() {
2124                                acc += hr[j] * hj;
2125                            }
2126                            h_new[k] = if relu { acc.max(0.0) } else { acc.tanh() };
2127                        }
2128                        for k in 0..hidden {
2129                            h[k] = h_new[k];
2130                            *lo.add((b * seq + t) * out_width + dir * hidden + k) = h_new[k];
2131                        }
2132                    }
2133                    if carry {
2134                        let hout = std::slice::from_raw_parts_mut(
2135                            (h0p + b * hidden * 4) as *mut f32,
2136                            hidden,
2137                        );
2138                        hout.copy_from_slice(&h);
2139                    }
2140                });
2141            }
2142
2143            wih_cursor += dirs * wih_block;
2144            layer_in = layer_out;
2145            in_l = out_width;
2146        }
2147
2148        let dst_slice = std::slice::from_raw_parts_mut((bptr + dst) as *mut f32, layer_in.len());
2149        dst_slice.copy_from_slice(&layer_in);
2150    }
2151}
2152
2153/// Reference for `Op::Mamba2` — SSD scalar-decay SSM scan. Inputs (f32):
2154/// `x [B,S,H,P]`, `dt [B,S,H]`, `a [H]`, `b/c [B,S,H,N]`. Per `(batch, head)`
2155/// the state `S[P,N]` is zero-init: `dA=exp(dt·a)`, `S=dA·S + (dt·x)⊗b`,
2156/// `y=Σ_n S[:,n]·c[n]`. Output `[B,S,H,P]`.
2157#[allow(clippy::too_many_arguments)]
2158pub unsafe fn execute_mamba2_f32(
2159    x: usize,
2160    dt: usize,
2161    a: usize,
2162    b: usize,
2163    c: usize,
2164    dst: usize,
2165    batch: usize,
2166    seq: usize,
2167    heads: usize,
2168    head_dim: usize,
2169    state_size: usize,
2170    base: *mut u8,
2171) {
2172    let (bn, s, h, p, n) = (batch, seq, heads, head_dim, state_size);
2173    let bptr = base as usize;
2174    unsafe {
2175        let f32s = |off: usize, len: usize| -> &[f32] {
2176            std::slice::from_raw_parts((bptr + off) as *const f32, len)
2177        };
2178        let xs = f32s(x, bn * s * h * p);
2179        let dts = f32s(dt, bn * s * h);
2180        let am = f32s(a, h);
2181        let bm = f32s(b, bn * s * h * n);
2182        let cm = f32s(c, bn * s * h * n);
2183        let out_ptr = bptr + dst;
2184
2185        // Independent per (batch, head).
2186        crate::pool::par_range(bn * h, |bh| {
2187            let bi = bh / h;
2188            let hi = bh % h;
2189            let out = out_ptr as *mut f32;
2190            let mut state = vec![0f32; p * n];
2191            for t in 0..s {
2192                let dt_t = dts[(bi * s + t) * h + hi];
2193                let da = (dt_t * am[hi]).exp();
2194                let x_off = ((bi * s + t) * h + hi) * p;
2195                let bc_off = ((bi * s + t) * h + hi) * n;
2196                for pi in 0..p {
2197                    let dtx = dt_t * xs[x_off + pi];
2198                    for ni in 0..n {
2199                        state[pi * n + ni] = da * state[pi * n + ni] + dtx * bm[bc_off + ni];
2200                    }
2201                }
2202                for pi in 0..p {
2203                    let mut acc = 0f32;
2204                    for ni in 0..n {
2205                        acc += state[pi * n + ni] * cm[bc_off + ni];
2206                    }
2207                    *out.add(x_off + pi) = acc;
2208                }
2209            }
2210        });
2211    }
2212}
2213
2214/// Mamba selective-scan reference (f32). Shared by the CPU `Thunk::SelectiveScan`
2215/// arm and the Metal unified-memory host fallback. `base` is the arena byte
2216/// pointer; the five inputs (`x`, `delta`, `a`, `b`, `c`) and `dst` are byte
2217/// offsets into it. Recurrence per `(batch, seq, channel, state)`:
2218/// `h[t] = exp(Δ·A)·h[t-1] + Δ·B·x;  y = Σ_n C·h`. State resets per batch row.
2219pub unsafe fn execute_selective_scan_f32(
2220    x: usize,
2221    delta: usize,
2222    a: usize,
2223    b: usize,
2224    c: usize,
2225    dst: usize,
2226    batch: usize,
2227    seq: usize,
2228    hidden: usize,
2229    state_size: usize,
2230    base: *mut u8,
2231) {
2232    let (bn, s, h, n) = (batch, seq, hidden, state_size);
2233    unsafe {
2234        let xs = sl(x, base, bn * s * h);
2235        let dt = sl(delta, base, bn * s * h);
2236        let am = sl(a, base, h * n);
2237        let bm = sl(b, base, bn * s * n);
2238        let cm = sl(c, base, bn * s * n);
2239        let out = sl_mut(dst, base, bn * s * h);
2240
2241        // State buffer per-batch: h channels × n state. Sequential along
2242        // the seq dimension; reset at the start of each batch row.
2243        let mut state = vec![0f32; h * n];
2244        for bi in 0..bn {
2245            for v in state.iter_mut() {
2246                *v = 0.0;
2247            }
2248            for si in 0..s {
2249                let x_row = &xs[bi * s * h + si * h..bi * s * h + (si + 1) * h];
2250                let dt_row = &dt[bi * s * h + si * h..bi * s * h + (si + 1) * h];
2251                let b_row = &bm[bi * s * n + si * n..bi * s * n + (si + 1) * n];
2252                let c_row = &cm[bi * s * n + si * n..bi * s * n + (si + 1) * n];
2253                let out_row = &mut out[bi * s * h + si * h..bi * s * h + (si + 1) * h];
2254
2255                for ci in 0..h {
2256                    let d = dt_row[ci];
2257                    let xv = x_row[ci];
2258                    let mut acc = 0f32;
2259                    for ni in 0..n {
2260                        // Discretize: exp(d * a) and d * b.
2261                        let da = (d * am[ci * n + ni]).exp();
2262                        state[ci * n + ni] = da * state[ci * n + ni] + d * b_row[ni] * xv;
2263                        acc += c_row[ni] * state[ci * n + ni];
2264                    }
2265                    out_row[ci] = acc;
2266                }
2267            }
2268        }
2269    }
2270}
2271
2272pub unsafe fn execute_gated_delta_net_f32(
2273    q: usize,
2274    k: usize,
2275    v: usize,
2276    g: usize,
2277    beta: usize,
2278    state: usize,
2279    dst: usize,
2280    batch: usize,
2281    seq: usize,
2282    heads: usize,
2283    state_size: usize,
2284    base: *mut u8,
2285) {
2286    #[derive(Copy, Clone)]
2287    struct ArenaPtr(usize);
2288    unsafe impl Send for ArenaPtr {}
2289    unsafe impl Sync for ArenaPtr {}
2290    impl ArenaPtr {
2291        #[inline]
2292        fn get(self) -> *mut u8 {
2293            self.0 as *mut u8
2294        }
2295    }
2296
2297    unsafe {
2298        let arena = ArenaPtr(base as usize);
2299        let (b, s, h, n) = (batch, seq, heads, state_size);
2300        let scale = 1.0f32 / (n as f32).sqrt();
2301        let use_external = state != 0;
2302        let mut owned_state = vec![0f32; h * n * n];
2303
2304        crate::pool::num_threads();
2305
2306        assert!(
2307            n <= crate::gdn::GDN_MAX_STATE,
2308            "GatedDeltaNet state_size={n} exceeds stack scratch ({})",
2309            crate::gdn::GDN_MAX_STATE
2310        );
2311
2312        let qs = sl(q, arena.get(), b * s * h * n);
2313        let ks = sl(k, arena.get(), b * s * h * n);
2314        let vs = sl(v, arena.get(), b * s * h * n);
2315        let gs = sl(g, arena.get(), b * s * h);
2316        let betas = sl(beta, arena.get(), b * s * h);
2317        let _out = sl_mut(dst, arena.get(), b * s * h * n);
2318        let hs_n = h * n;
2319
2320        let run_head = |bi: usize, hi: usize, s_mat: &mut [f32], sk: &mut [f32]| {
2321            for ti in 0..s {
2322                let qkv_step = bi * s * hs_n + ti * hs_n + hi * n;
2323                let gb_step = bi * s * h + ti * h + hi;
2324                let out_row = sl_mut(dst + qkv_step * std::mem::size_of::<f32>(), arena.get(), n);
2325                crate::gdn::gdn_step_blas(
2326                    s_mat,
2327                    &qs[qkv_step..qkv_step + n],
2328                    &ks[qkv_step..qkv_step + n],
2329                    &vs[qkv_step..qkv_step + n],
2330                    gs[gb_step],
2331                    betas[gb_step],
2332                    out_row,
2333                    sk,
2334                    n,
2335                    scale,
2336                );
2337            }
2338        };
2339
2340        // Prefill (seq>1, ephemeral state): time-outer, parallel over heads —
2341        // better occupancy than head-outer when prompt length dominates.
2342        if !use_external && s > 1 {
2343            for bi in 0..b {
2344                crate::pool::par_range(h, |hi| {
2345                    let mut sk_buf = [0f32; crate::gdn::GDN_MAX_STATE];
2346                    let sk = &mut sk_buf[..n];
2347                    let mut local_state =
2348                        [0f32; crate::gdn::GDN_MAX_STATE * crate::gdn::GDN_MAX_STATE];
2349                    let s_mat = &mut local_state[..n * n];
2350                    s_mat.fill(0.0);
2351                    run_head(bi, hi, s_mat, sk);
2352                });
2353            }
2354            return;
2355        }
2356
2357        if use_external {
2358            let state_bytes = state;
2359            crate::pool::par_range(b * h, |bhi| {
2360                let bi = bhi / h;
2361                let hi = bhi % h;
2362                let elem_off = bi * h * n * n + hi * n * n;
2363                let s_mat = sl_mut(
2364                    state_bytes + elem_off * std::mem::size_of::<f32>(),
2365                    arena.get(),
2366                    n * n,
2367                );
2368                let mut sk_buf = [0f32; crate::gdn::GDN_MAX_STATE];
2369                run_head(bi, hi, s_mat, &mut sk_buf[..n]);
2370            });
2371        } else {
2372            for bi in 0..b {
2373                owned_state.fill(0.0);
2374                #[cfg(not(target_arch = "wasm32"))]
2375                {
2376                    use rayon::prelude::*;
2377                    owned_state
2378                        .par_chunks_mut(n * n)
2379                        .enumerate()
2380                        .for_each(|(hi, s_mat)| {
2381                            let mut sk_buf = [0f32; crate::gdn::GDN_MAX_STATE];
2382                            run_head(bi, hi, s_mat, &mut sk_buf[..n]);
2383                        });
2384                }
2385                #[cfg(target_arch = "wasm32")]
2386                {
2387                    owned_state
2388                        .chunks_mut(n * n)
2389                        .enumerate()
2390                        .for_each(|(hi, s_mat)| {
2391                            let mut sk_buf = [0f32; crate::gdn::GDN_MAX_STATE];
2392                            run_head(bi, hi, s_mat, &mut sk_buf[..n]);
2393                        });
2394                }
2395            }
2396        }
2397    }
2398}
2399
2400/// Host-fallback entry for f16 `Op::GatedDeltaNet` tensors on Metal.
2401pub unsafe fn execute_gated_delta_net_f16(
2402    q: usize,
2403    k: usize,
2404    v: usize,
2405    g: usize,
2406    beta: usize,
2407    state: usize,
2408    dst: usize,
2409    batch: usize,
2410    seq: usize,
2411    heads: usize,
2412    state_size: usize,
2413    base: *mut u8,
2414) {
2415    use half::f16;
2416    unsafe {
2417        let read_f16 = |off: usize, len: usize| -> Vec<f32> {
2418            let raw = std::slice::from_raw_parts(base.add(off) as *const u8, len * 2);
2419            raw.chunks_exact(2)
2420                .map(|c| f16::from_le_bytes([c[0], c[1]]).to_f32())
2421                .collect()
2422        };
2423        let write_f16 = |off: usize, data: &[f32]| {
2424            let out = std::slice::from_raw_parts_mut(base.add(off), data.len() * 2);
2425            for (i, &v) in data.iter().enumerate() {
2426                let le = f16::from_f32(v).to_le_bytes();
2427                out[i * 2] = le[0];
2428                out[i * 2 + 1] = le[1];
2429            }
2430        };
2431
2432        let (b, s, h, n) = (batch, seq, heads, state_size);
2433        let q_f = read_f16(q, b * s * h * n);
2434        let k_f = read_f16(k, b * s * h * n);
2435        let v_f = read_f16(v, b * s * h * n);
2436        let g_f = read_f16(g, b * s * h);
2437        let b_f = read_f16(beta, b * s * h);
2438        let mut state_f = if state != 0 {
2439            read_f16(state, b * h * n * n)
2440        } else {
2441            vec![0f32; b * h * n * n]
2442        };
2443        let mut out_f = vec![0f32; b * s * h * n];
2444        let scale = 1.0f32 / (n as f32).sqrt();
2445        let mut sk_buf = vec![0f32; n];
2446        let mut owned_state = vec![0f32; h * n * n];
2447
2448        for bi in 0..b {
2449            let state_slice: &mut [f32] = if state != 0 {
2450                let start = bi * h * n * n;
2451                &mut state_f[start..start + h * n * n]
2452            } else {
2453                owned_state.fill(0.0);
2454                &mut owned_state
2455            };
2456
2457            for ti in 0..s {
2458                let qkv_step_base = bi * s * h * n + ti * h * n;
2459                let gb_step_base = bi * s * h + ti * h;
2460
2461                for hi in 0..h {
2462                    let q_row = &q_f[qkv_step_base + hi * n..qkv_step_base + (hi + 1) * n];
2463                    let k_row = &k_f[qkv_step_base + hi * n..qkv_step_base + (hi + 1) * n];
2464                    let v_row = &v_f[qkv_step_base + hi * n..qkv_step_base + (hi + 1) * n];
2465                    let g_t = g_f[gb_step_base + hi];
2466                    let beta_t = b_f[gb_step_base + hi];
2467
2468                    let s_base = hi * n * n;
2469                    let s_mat = &mut state_slice[s_base..s_base + n * n];
2470
2471                    let g_exp = g_t.exp();
2472                    for st in s_mat.iter_mut() {
2473                        *st *= g_exp;
2474                    }
2475
2476                    for j in 0..n {
2477                        let mut acc = 0f32;
2478                        for i in 0..n {
2479                            acc += s_mat[i * n + j] * k_row[i];
2480                        }
2481                        sk_buf[j] = acc;
2482                    }
2483
2484                    for j in 0..n {
2485                        sk_buf[j] = (v_row[j] - sk_buf[j]) * beta_t;
2486                    }
2487
2488                    for i in 0..n {
2489                        let ki = k_row[i];
2490                        for j in 0..n {
2491                            s_mat[i * n + j] += ki * sk_buf[j];
2492                        }
2493                    }
2494
2495                    let out_row = &mut out_f[qkv_step_base + hi * n..qkv_step_base + (hi + 1) * n];
2496                    for j in 0..n {
2497                        let mut acc = 0f32;
2498                        for i in 0..n {
2499                            acc += s_mat[i * n + j] * q_row[i];
2500                        }
2501                        out_row[j] = acc * scale;
2502                    }
2503                }
2504            }
2505        }
2506
2507        write_f16(dst, &out_f);
2508        if state != 0 {
2509            write_f16(state, &state_f);
2510        }
2511    }
2512}