Skip to main content

rlx_cpu/thunk/
exec_dispatch.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3// SPDX-License-Identifier: MIT OR Apache-2.0
4#![allow(unsafe_op_in_unsafe_fn)]
5use crate::thunk::*;
6
7/// Execute a thunk schedule on a raw arena buffer.
8/// Fastest executor: call pre-compiled closures sequentially.
9/// Zero match dispatch — each closure is a direct kernel call.
10pub fn execute_compiled(schedule: &ThunkSchedule, arena_buf: &mut [u8]) {
11    let base = arena_buf.as_mut_ptr();
12    // `RLX_CPU_WATCH=<byte_offset>:<n_floats>` reports every thunk that changes
13    // the watched region. A node read by one thunk and written by a LATER one
14    // is a scheduling hazard the post-run arena dump cannot see: it shows the
15    // final value, so the reader looks like it was fed data it never got.
16    if let Some(spec) = rlx_ir::env::var("RLX_CPU_WATCH") {
17        let mut it = spec
18            .split(':')
19            .filter_map(|t| t.trim().parse::<usize>().ok());
20        if let (Some(off), Some(len)) = (it.next(), it.next()) {
21            let mut prev: Option<Vec<f32>> = None;
22            for (i, f) in schedule.compiled_fns.iter().enumerate() {
23                f(base);
24                let cur: Vec<f32> =
25                    unsafe { std::slice::from_raw_parts(base.add(off) as *const f32, len) }
26                        .to_vec();
27                if prev.as_deref() != Some(cur.as_slice()) {
28                    let tabs: f64 = cur.iter().map(|&v| v.abs() as f64).sum();
29                    eprintln!(
30                        "[rlx-cpu-watch] thunk {i} {:?} changed watch: tabs={tabs:.6}",
31                        schedule.thunks.get(i).map(std::mem::discriminant)
32                    );
33                    prev = Some(cur);
34                }
35            }
36            return;
37        }
38    }
39    for f in &schedule.compiled_fns {
40        f(base);
41    }
42}
43
44/// Active-extent execution stub. The runtime calls this when it has an
45/// active-extent hint set. CPU doesn't implement per-thunk active-extent
46/// scaling yet — return false so the caller falls back to the full
47/// `execute_thunks` path.
48pub fn execute_thunks_active(
49    schedule: &ThunkSchedule,
50    _arena_buf: &mut [u8],
51    _actual: usize,
52    _upper: usize,
53) -> bool {
54    let _ = schedule;
55    false
56}
57
58/// Match-based executor (fallback, used by tests).
59pub(crate) struct MoeResidencyGuard;
60impl Drop for MoeResidencyGuard {
61    fn drop(&mut self) {
62        if let Some(stats) = crate::moe_residency::take_stats() {
63            crate::moe_residency::stash_last_forward_stats(stats);
64        } else {
65            crate::moe_residency::clear_mask();
66        }
67    }
68}
69
70/// Contiguous AVX2 Add/Mul/Sub. Large buffers are split across Rayon so we
71/// keep SIMD *and* multi-core (serial AVX2 alone lost to 16-wide scalar rayon
72/// on F5-TTS DiT residuals).
73#[cfg(target_arch = "x86_64")]
74#[inline]
75fn binary_contig_f32(l: &[f32], r: &[f32], o: &mut [f32], op: BinaryOp) -> bool {
76    if !std::arch::is_x86_feature_detected!("avx2") {
77        return false;
78    }
79    if !matches!(op, BinaryOp::Add | BinaryOp::Mul | BinaryOp::Sub) {
80        return false;
81    }
82    let len = o.len();
83    let l_ptr = l.as_ptr() as usize;
84    let r_ptr = r.as_ptr() as usize;
85    let o_ptr = o.as_mut_ptr() as usize;
86    let run = |i0: usize, i1: usize| unsafe {
87        // SAFETY: AVX2 checked above, `op` is Add/Sub/Mul, and `[i0, i1)` is
88        // within `len` — the bound every caller below is derived from.
89        binary_contig_avx2(
90            l_ptr as *const f32,
91            r_ptr as *const f32,
92            o_ptr as *mut f32,
93            i0,
94            i1,
95            op,
96        )
97    };
98    if len >= 8192 && crate::pool::num_threads() > 1 {
99        crate::pool::par_for(len, crate::pool::chunk_floor(len), &|off, cnt| {
100            run(off, off + cnt);
101        });
102    } else {
103        run(0, len);
104    }
105    true
106}
107
108/// 8-wide Add/Sub/Mul over `[i0, i1)` of three raw f32 buffers.
109///
110/// Carries `#[target_feature(enable = "avx2")]` for a reason that is invisible
111/// in the source and obvious in the disassembly: `_mm256_*` are themselves
112/// `#[target_feature]` fns, so a caller that merely *checks* AVX2 at runtime
113/// cannot inline them — each becomes a `call` plus a `vzeroupper`, four per 8
114/// lanes. Measured that way this path ran at ~7-9 GB/s against ~70-124 GB/s
115/// for the plain scalar loop it exists to beat, i.e. the "fast path" was an
116/// 8-18x pessimization. With the attribute here the intrinsics inline and it
117/// matches the hand-written-SIMD number. Same shape as every other AVX2
118/// kernel in this crate (`silu_inplace_avx2`, `softmax_rows_avx2`, …): tiny
119/// runtime-gated wrapper, `#[target_feature]` body.
120///
121/// # Safety
122/// Caller has checked `is_x86_feature_detected!("avx2")`, `op` is one of
123/// Add/Sub/Mul, and `[i0, i1)` is in bounds of all three buffers.
124#[cfg(target_arch = "x86_64")]
125#[target_feature(enable = "avx2")]
126#[allow(unsafe_op_in_unsafe_fn)]
127unsafe fn binary_contig_avx2(
128    l: *const f32,
129    r: *const f32,
130    o: *mut f32,
131    i0: usize,
132    i1: usize,
133    op: BinaryOp,
134) {
135    use std::arch::x86_64::*;
136    let mut i = i0;
137    // Align to 8-wide when possible within the chunk.
138    while i + 8 <= i1 {
139        let a = _mm256_loadu_ps(l.add(i));
140        let b = _mm256_loadu_ps(r.add(i));
141        let res = match op {
142            BinaryOp::Add => _mm256_add_ps(a, b),
143            BinaryOp::Sub => _mm256_sub_ps(a, b),
144            BinaryOp::Mul => _mm256_mul_ps(a, b),
145            _ => unreachable!(),
146        };
147        _mm256_storeu_ps(o.add(i), res);
148        i += 8;
149    }
150    while i < i1 {
151        let a = *l.add(i);
152        let b = *r.add(i);
153        *o.add(i) = match op {
154            BinaryOp::Add => a + b,
155            BinaryOp::Sub => a - b,
156            BinaryOp::Mul => a * b,
157            _ => unreachable!(),
158        };
159        i += 1;
160    }
161}
162
163#[cfg(not(target_arch = "x86_64"))]
164#[inline]
165#[allow(dead_code)]
166fn binary_contig_f32(_l: &[f32], _r: &[f32], _o: &mut [f32], _op: BinaryOp) -> bool {
167    false
168}
169
170/// Row-tiled rhs broadcast: `o[i] = op(l[i], r[i % rl])` with optional AVX2.
171#[inline]
172fn binary_row_bcast_f32(l: &[f32], r: &[f32], o: &mut [f32], op: BinaryOp, rl: usize) -> bool {
173    let len = o.len();
174    if rl == 0 || rl >= len || !len.is_multiple_of(rl) || l.len() < len || r.len() < rl {
175        return false;
176    }
177    let rows = len / rl;
178    let l_ptr = l.as_ptr() as usize;
179    let r_ptr = r.as_ptr() as usize;
180    let o_ptr = o.as_mut_ptr() as usize;
181
182    #[cfg(target_arch = "x86_64")]
183    let use_avx2 = rl >= 8
184        && rl.is_multiple_of(8)
185        && matches!(op, BinaryOp::Add | BinaryOp::Mul | BinaryOp::Sub)
186        && std::arch::is_x86_feature_detected!("avx2");
187
188    let run_rows = |row0: usize, row1: usize| unsafe {
189        let l = l_ptr as *const f32;
190        let r = r_ptr as *const f32;
191        let o = o_ptr as *mut f32;
192        #[cfg(target_arch = "x86_64")]
193        if use_avx2 {
194            // SAFETY: AVX2 + `rl % 8 == 0` + Add/Sub/Mul all checked in
195            // `use_avx2`; rows `[row0, row1)` are within `rows`.
196            binary_row_bcast_avx2(l, r, o, row0, row1, rl, op);
197            return;
198        }
199        for row in row0..row1 {
200            let base = row * rl;
201            for j in 0..rl {
202                let i = base + j;
203                let a = *l.add(i);
204                let b = *r.add(j);
205                *o.add(i) = match op {
206                    BinaryOp::Add => a + b,
207                    BinaryOp::Sub => a - b,
208                    BinaryOp::Mul => a * b,
209                    BinaryOp::Div => a / b,
210                    BinaryOp::Max => a.max(b),
211                    BinaryOp::Min => a.min(b),
212                    BinaryOp::Pow => a.powf(b),
213                    BinaryOp::Mod => a % b,
214                    BinaryOp::Atan2 => a.atan2(b),
215                    BinaryOp::BitAnd => ((a as i64) & (b as i64)) as f32,
216                    BinaryOp::BitOr => ((a as i64) | (b as i64)) as f32,
217                    BinaryOp::BitXor => ((a as i64) ^ (b as i64)) as f32,
218                    BinaryOp::Shl => (a as i64).wrapping_shl(b as u32) as f32,
219                    BinaryOp::Shr => (a as i64).wrapping_shr(b as u32) as f32,
220                };
221            }
222        }
223    };
224    if rows >= 4 && crate::pool::num_threads() > 1 && len >= 8192 {
225        crate::pool::par_for(rows, 1, &|off, cnt| run_rows(off, off + cnt));
226    } else {
227        run_rows(0, rows);
228    }
229    true
230}
231
232/// Row-broadcast `o[row*rl + j] = op(l[row*rl + j], r[j])`, 8 lanes at a time.
233///
234/// Split out for the same reason as [`binary_contig_avx2`] — the intrinsics
235/// only inline inside a fn that actually has AVX2 enabled.
236///
237/// # Safety
238/// Caller has checked `is_x86_feature_detected!("avx2")`, that `rl` is a
239/// non-zero multiple of 8, that `op` is one of Add/Sub/Mul, and that rows
240/// `[row0, row1)` are in bounds of all three buffers.
241#[cfg(target_arch = "x86_64")]
242#[target_feature(enable = "avx2")]
243#[allow(unsafe_op_in_unsafe_fn)]
244unsafe fn binary_row_bcast_avx2(
245    l: *const f32,
246    r: *const f32,
247    o: *mut f32,
248    row0: usize,
249    row1: usize,
250    rl: usize,
251    op: BinaryOp,
252) {
253    use std::arch::x86_64::*;
254    let chunks = rl / 8;
255    for row in row0..row1 {
256        let base = row * rl;
257        for c in 0..chunks {
258            let off = base + c * 8;
259            let roff = c * 8;
260            let a = _mm256_loadu_ps(l.add(off));
261            let b = _mm256_loadu_ps(r.add(roff));
262            let res = match op {
263                BinaryOp::Add => _mm256_add_ps(a, b),
264                BinaryOp::Sub => _mm256_sub_ps(a, b),
265                BinaryOp::Mul => _mm256_mul_ps(a, b),
266                _ => unreachable!(),
267            };
268            _mm256_storeu_ps(o.add(off), res);
269        }
270    }
271}
272
273pub(crate) fn thunk_kind_name(t: &Thunk) -> &'static str {
274    // Zero-alloc variant name from the derived `IntoStaticStr` on `Thunk`
275    // (see its definition). Covers EVERY variant automatically, so a newly
276    // added thunk is labeled by name instead of silently bucketing into
277    // "Other" — which had hidden the pathologically slow AttentionBackward.
278    <&'static str>::from(t)
279}
280
281/// Per-thunk-kind wall-time accumulator, populated only when the env var
282/// `RLX_PROFILE_THUNKS` is set. Used to see which ops dominate a step so the
283/// optimizer/kernels can target the real hotspots rather than guesses.
284pub(crate) static THUNK_PROFILE: std::sync::Mutex<
285    Option<std::collections::BTreeMap<&'static str, (u128, u64)>>,
286> = std::sync::Mutex::new(None);
287
288#[inline]
289pub(crate) fn profile_record(name: &'static str, d: std::time::Duration) {
290    let mut g = THUNK_PROFILE.lock().unwrap();
291    let map = g.get_or_insert_with(std::collections::BTreeMap::new);
292    let e = map.entry(name).or_insert((0, 0));
293    e.0 += d.as_nanos();
294    e.1 += 1;
295}
296
297/// Print and clear the per-thunk-kind time profile gathered under
298/// `RLX_PROFILE_THUNKS`. Call after a run to see where the time went.
299pub fn dump_thunk_profile() {
300    let mut g = THUNK_PROFILE.lock().unwrap();
301    if let Some(map) = g.take() {
302        let mut v: Vec<_> = map.into_iter().collect();
303        v.sort_by_key(|b| std::cmp::Reverse(b.1.0));
304        let total: u128 = v.iter().map(|(_, (ns, _))| *ns).sum();
305        eprintln!(
306            "[thunk-profile] total {:.1}ms across kinds:",
307            total as f64 / 1e6
308        );
309        for (name, (ns, c)) in v.iter().take(25) {
310            eprintln!("  {name:<28} {:>8.1}ms  ({c} calls)", *ns as f64 / 1e6);
311        }
312    }
313}
314
315/// Do the arena byte ranges `[a, a + a_elems*4)` and `[c, c + c_elems*4)`
316/// overlap at all?
317///
318/// The BLAS aliasing guard has to compare *ranges*, not pointers. When a
319/// fusion folds a `Transpose` into a matmul, the surviving operand is the
320/// transpose's *source* buffer, which the planner scored as dead at the
321/// transpose — so its slot is free to be reused for the matmul's own output.
322/// That leaves an operand sitting strictly inside the destination (WavTokenizer
323/// attention: `probs @ V` reads a 256 B `probs` from the middle of its own
324/// 24 KB output slot). `ptr::eq` sees two different addresses and waves it
325/// through, after which cblas streams `probs` while overwriting it. Accelerate
326/// happens to pack its operands up front and survives; OpenBLAS does not, which
327/// is why this only ever corrupted x86_64.
328#[inline]
329fn arena_ranges_overlap(a: usize, a_elems: usize, c: usize, c_elems: usize) -> bool {
330    a < c + c_elems * 4 && c < a + a_elems * 4
331}
332
333/// The same range test where only the slices are in hand.
334///
335/// The batched matmul paths build their per-item slices from raw pointers, so
336/// they have no arena offsets to compare — and they were left on `ptr::eq` when
337/// the non-batched paths were fixed. A batched attention (`probs @ V` per head)
338/// is exactly where an operand ends up strictly inside its own output slot, so
339/// the weaker guard let cblas stream an operand it was simultaneously
340/// overwriting: a band of query rows corrupted, varying run to run with the
341/// order BLAS happened to pack in, and cured by `RLX_ARENA_NO_REUSE=1`.
342#[inline]
343fn slices_overlap(a: &[f32], c: &[f32]) -> bool {
344    let (a0, a1) = (a.as_ptr() as usize, a.as_ptr() as usize + a.len() * 4);
345    let (c0, c1) = (c.as_ptr() as usize, c.as_ptr() as usize + c.len() * 4);
346    a0 < c1 && c0 < a1
347}
348
349pub fn execute_thunks(schedule: &ThunkSchedule, arena_buf: &mut [u8]) {
350    // `RLX_CPU_EXEC_IN_POOL`: run the whole schedule on a pool worker. Called
351    // from outside the pool, every parallel kernel parks this thread until the
352    // workers finish and then waits for it to be woken — per GEMV, per
353    // attention, ~225 times per decoded token. On a worker the kernels' joins
354    // spin instead, and this thread's share of the work is done by the worker
355    // it runs on. Thread-local kernel state (MoE residency masks, counters)
356    // then lives on that worker.
357    #[cfg(not(target_arch = "wasm32"))]
358    if exec_in_pool() && rayon::current_thread_index().is_none() && crate::pool::num_threads() > 1 {
359        return rayon::scope(|_| execute_thunks_here(schedule, arena_buf));
360    }
361    execute_thunks_here(schedule, arena_buf)
362}
363
364#[cfg(not(target_arch = "wasm32"))]
365fn exec_in_pool() -> bool {
366    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
367    *ON.get_or_init(|| rlx_ir::env::flag("RLX_CPU_EXEC_IN_POOL"))
368}
369
370fn execute_thunks_here(schedule: &ThunkSchedule, arena_buf: &mut [u8]) {
371    crate::moe_residency::reset_gmm_counters();
372    if let Some(layers) = schedule.moe_resident_layers.clone() {
373        crate::moe_residency::set_per_layer_masks(Some(layers));
374    } else {
375        crate::moe_residency::set_mask(schedule.moe_resident.clone());
376    }
377    if let Some(cap) = schedule.moe_topk_capture.as_ref() {
378        cap.clear();
379    }
380    let _moe_guard = MoeResidencyGuard;
381    let base = arena_buf.as_mut_ptr();
382    let mask_thr = schedule.mask_threshold;
383    let mask_neg = schedule.mask_neg_inf;
384    let score_thr = schedule.score_skip;
385    let thunks = &schedule.thunks;
386    let len = thunks.len();
387
388    // Pre-allocate ALL reusable buffers once (zero per-call allocation)
389    let max_h = thunks
390        .iter()
391        .filter_map(|t| match t {
392            Thunk::FusedResidualLN { h, .. }
393            | Thunk::FusedResidualRmsNorm { h, .. }
394            | Thunk::LayerNorm { h, .. } => Some(*h as usize),
395            _ => None,
396        })
397        .max()
398        .unwrap_or(0);
399    let zero_bias = vec![0f32; max_h];
400
401    // Pre-allocate per-(batch,head) score buffers for parallel SDPA.
402    // Q/K/V/out are accessed via strided BLAS — no deinterleave copy needed.
403    let max_sdpa = thunks
404        .iter()
405        .filter_map(|t| match t {
406            Thunk::Attention {
407                batch,
408                seq,
409                kv_seq,
410                heads,
411                head_dim,
412                ..
413            } => Some((
414                *batch as usize,
415                (*seq as usize).max(*kv_seq as usize),
416                *heads as usize,
417                *head_dim as usize,
418            )),
419            _ => None,
420        })
421        .fold((0, 0, 0, 0), |(mb, ms, mh, md), (b, s, h, d)| {
422            (mb.max(b), ms.max(s), mh.max(h), md.max(d))
423        });
424    let (max_batch, max_seq, max_heads, _max_dh) = max_sdpa;
425    let max_units = max_batch * max_heads;
426    let mut sdpa_scores = vec![0f32; max_units * max_seq * max_seq];
427
428    // Pre-allocate fused layer buffers (reused across all 12+ layers — zero malloc per layer)
429    let fl = thunks
430        .iter()
431        .filter_map(|t| match t {
432            Thunk::FusedBertLayer {
433                batch,
434                seq,
435                hs,
436                int_dim,
437                ..
438            } => {
439                let m = (*batch as usize) * (*seq as usize);
440                let h = *hs as usize;
441                let id = *int_dim as usize;
442                Some((m, h, id, m * (*seq as usize)))
443            }
444            Thunk::FusedNomicLayer {
445                batch,
446                seq,
447                hs,
448                int_dim,
449                ..
450            } => {
451                let m = (*batch as usize) * (*seq as usize);
452                let h = *hs as usize;
453                let id = *int_dim as usize;
454                Some((m, h, id, m * (*seq as usize)))
455            }
456            _ => None,
457        })
458        .fold((0, 0, 0, 0), |(mm, mh, mi, ms), (m, h, id, ss)| {
459            (mm.max(m), mh.max(h), mi.max(id), ms.max(ss))
460        });
461    let (fl_m, fl_h, fl_int, fl_ss) = fl;
462    let mut fl_qkv = vec![0f32; fl_m * 3 * fl_h];
463    let mut fl_attn = vec![0f32; fl_m * fl_h];
464    let mut fl_res = vec![0f32; fl_m * fl_h];
465    let mut fl_normed = vec![0f32; fl_m * fl_h];
466    let mut fl_ffn = vec![0f32; fl_m * fl_int.max(2 * fl_int)]; // Nomic needs 2×int for fused fc11+fc12
467    let mut fl_sc = vec![0f32; fl_ss.max(1)];
468
469    let trace_thunks = rlx_ir::env::var_os("RLX_TRACE_THUNK").is_some();
470    if trace_thunks {
471        eprintln!(
472            "[thunk] prealloc max_h={max_h} sdpa={} fl_m={fl_m} fl_h={fl_h} fl_int={fl_int}",
473            max_units * max_seq * max_seq
474        );
475    }
476    let profile = rlx_ir::env::var_os("RLX_PROFILE_THUNKS").is_some();
477    // Time the previous thunk at the top of each iteration (avoids touching the
478    // giant match's many arms). The last thunk's tail is folded into the next
479    // step's first sample — negligible over a training run.
480    let mut prof_prev: Option<(&'static str, std::time::Instant)> = None;
481    // Byte-range write tracer (`RLX_ARENA_WRITE_TRACE=1`). Snapshots the arena
482    // around every thunk and reports the exact range each one dirtied, so a
483    // kernel that writes past its planned slot — the classic SIMD-tail overrun
484    // that corrupts whichever buffer the planner reused next — is named
485    // directly instead of inferred. O(arena) per thunk, so diagnostics only.
486    let write_trace = rlx_ir::env::var_os("RLX_ARENA_WRITE_TRACE").is_some();
487    let mut wt_before: Vec<u8> = if write_trace {
488        arena_buf.to_vec()
489    } else {
490        Vec::new()
491    };
492    // `RLX_CPU_WATCH=<byte_offset>:<n_floats>` reports the watched region's
493    // contents whenever they change, naming the thunk about to run — so the
494    // writer is the thunk *before* the one printed. A node written after its
495    // reader is a scheduling hazard invisible to the post-run arena dump,
496    // which only ever shows the final value.
497    let watch = rlx_ir::env::var("RLX_CPU_WATCH").and_then(|spec| {
498        let mut it = spec
499            .split(':')
500            .filter_map(|t| t.trim().parse::<usize>().ok());
501        Some((it.next()?, it.next()?))
502    });
503    let mut watch_prev: Option<Vec<f32>> = None;
504    // `RLX_CPU_DUMP_THUNKS=<lo>:<hi>` prints the compiled thunks in that index
505    // range, with their arena offsets and dimensions — the arguments a kernel
506    // actually receives, as opposed to the ones the graph implies.
507    if let Some(spec) = rlx_ir::env::var("RLX_CPU_DUMP_THUNKS") {
508        let mut it = spec
509            .split(':')
510            .filter_map(|t| t.trim().parse::<usize>().ok());
511        if let (Some(lo), Some(hi)) = (it.next(), it.next()) {
512            for (i, t) in thunks.iter().enumerate().take(hi.min(len)).skip(lo) {
513                let detail = match t {
514                    Thunk::FusedMmBiasAct {
515                        a,
516                        w,
517                        bias,
518                        c,
519                        m,
520                        k,
521                        n,
522                        act,
523                    } => format!(" a={a} w={w} bias={bias} c={c} m={m} k={k} n={n} act={act:?}"),
524                    Thunk::Sgemm { a, b, c, m, k, n } => {
525                        format!(" a={a} b={b} c={c} m={m} k={k} n={n}")
526                    }
527                    _ => String::new(),
528                };
529                eprintln!("[rlx-cpu-thunk] {i} {}{detail}", thunk_kind_name(t));
530            }
531        }
532    }
533    for i in 0..len {
534        if profile {
535            if let Some((pn, pt)) = prof_prev.take() {
536                profile_record(pn, pt.elapsed());
537            }
538        }
539        let thunk = unsafe { thunks.get_unchecked(i) };
540        if let Some((woff, wlen)) = watch {
541            let cur: Vec<f32> =
542                unsafe { std::slice::from_raw_parts(base.add(woff) as *const f32, wlen) }.to_vec();
543            if watch_prev.as_deref() != Some(cur.as_slice()) {
544                let tabs: f64 = cur.iter().map(|&v| v.abs() as f64).sum();
545                eprintln!(
546                    "[rlx-cpu-watch] before thunk {i} ({}) tabs={tabs:.6}",
547                    thunk_kind_name(thunk)
548                );
549                watch_prev = Some(cur);
550            }
551        }
552        if trace_thunks && (i < 120 || i % 200 == 0 || i + 1 == len) {
553            eprintln!("[thunk {i}/{len}] {}", thunk_kind_name(thunk));
554        }
555        let trace_done = trace_thunks && i < 120;
556        if profile {
557            prof_prev = Some((thunk_kind_name(thunk), std::time::Instant::now()));
558        }
559        match thunk {
560            Thunk::Nop => exec_nop(thunk),
561            Thunk::ElementwiseRegion { .. } => exec_elementwise_region(thunk, base),
562            Thunk::GaussianSplatRender { .. } => exec_gaussian_splat_render(thunk, base),
563            Thunk::GaussianSplatRenderBackward { .. } => {
564                exec_gaussian_splat_render_backward(thunk, base)
565            }
566            Thunk::GaussianSplatPrepare { .. } => exec_gaussian_splat_prepare(thunk, base),
567            Thunk::GaussianSplatRasterize { .. } => exec_gaussian_splat_rasterize(thunk, base),
568            Thunk::Fft1d { .. } => exec_fft1d(thunk, base),
569            Thunk::Fft1dQ { .. } => exec_fft1d_q(thunk, base),
570            Thunk::FftButterflyStage { .. } => exec_fft_butterfly_stage(thunk, base),
571            Thunk::LogMel { .. } => exec_log_mel(thunk, base),
572            Thunk::LogMelBackward { .. } => exec_log_mel_backward(thunk, base),
573            Thunk::WelchPeaks { .. } => exec_welch_peaks(thunk, base),
574            Thunk::CustomFn { .. } => exec_custom_fn(thunk, base),
575            Thunk::Sgemm { a, b, c, m, k, n } => {
576                let (m, k, n) = (*m as usize, *k as usize, *n as usize);
577                if trace_thunks {
578                    eprintln!("[sgemm] m={m} k={k} n={n} a={} b={} c={}", *a, *b, *c);
579                }
580                let c_len = m.saturating_mul(n);
581                let a_len = m.saturating_mul(k);
582                let b_len = k.saturating_mul(n);
583                let arena_len = arena_buf.len();
584                let max_a = (arena_len.saturating_sub(*a)) / 4;
585                let max_b = (arena_len.saturating_sub(*b)) / 4;
586                let max_c = (arena_len.saturating_sub(*c)) / 4;
587                let a_len = a_len.min(max_a);
588                let b_len = b_len.min(max_b);
589                let c_len = c_len.min(max_c);
590                unsafe {
591                    let a_sl = sl(*a, base, a_len);
592                    let b_sl = sl(*b, base, b_len);
593                    let c_sl = sl_mut(*c, base, c_len);
594                    if arena_ranges_overlap(*a, a_len, *c, c_len)
595                        || arena_ranges_overlap(*b, b_len, *c, c_len)
596                    {
597                        let mut tmp = vec![0.0f32; c_len];
598                        crate::blas::sgemm_auto(a_sl, b_sl, &mut tmp, m, k, n);
599                        c_sl.copy_from_slice(&tmp);
600                    } else {
601                        crate::blas::sgemm_auto(a_sl, b_sl, c_sl, m, k, n);
602                    }
603                }
604            }
605
606            Thunk::SgemmHalf {
607                a,
608                b,
609                c,
610                m,
611                k,
612                n,
613                a_kind,
614                b_kind,
615                c_kind,
616            } => {
617                let (m, k, n) = (*m as usize, *k as usize, *n as usize);
618                let (a_len, b_len, c_len) = (m * k, k * n, m * n);
619                unsafe {
620                    let a_buf;
621                    let a_sl: &[f32] = if a_kind.packed() {
622                        a_buf = crate::blas::widen_half_vec(
623                            sl_typed::<u16>(*a, base, a_len),
624                            *a_kind == crate::thunk::HalfKind::Bf16,
625                        );
626                        &a_buf
627                    } else {
628                        sl(*a, base, a_len)
629                    };
630                    let b_buf;
631                    let b_sl: &[f32] = if b_kind.packed() {
632                        b_buf = crate::blas::widen_half_vec(
633                            sl_typed::<u16>(*b, base, b_len),
634                            *b_kind == crate::thunk::HalfKind::Bf16,
635                        );
636                        &b_buf
637                    } else {
638                        sl(*b, base, b_len)
639                    };
640                    if c_kind.packed() {
641                        let mut tmp = vec![0f32; c_len];
642                        crate::blas::sgemm_auto(a_sl, b_sl, &mut tmp, m, k, n);
643                        let dst = sl_mut_typed::<u16>(*c, base, c_len);
644                        crate::blas::round_half_into(
645                            &tmp,
646                            dst,
647                            *c_kind == crate::thunk::HalfKind::Bf16,
648                        );
649                    } else {
650                        let c_sl = sl_mut(*c, base, c_len);
651                        crate::blas::sgemm_auto(a_sl, b_sl, c_sl, m, k, n);
652                    }
653                }
654            }
655
656            Thunk::SgemmBf16 { a, b, c, m, k, n } => {
657                let (m, k, n) = (*m as usize, *k as usize, *n as usize);
658                let c_len = m.saturating_mul(n);
659                let a_len = m.saturating_mul(k);
660                let bn = k.saturating_mul(n);
661                unsafe {
662                    let a_sl = sl(*a, base, a_len);
663                    let b16 = sl_typed::<u16>(*b, base, bn);
664                    let c_sl = sl_mut(*c, base, c_len);
665                    crate::blas::sgemm_bf16_rhs(a_sl, b16, c_sl, m, k, n);
666                }
667            }
668
669            Thunk::SgemmF16 { a, b, c, m, k, n } => {
670                let (m, k, n) = (*m as usize, *k as usize, *n as usize);
671                let c_len = m.saturating_mul(n);
672                let a_len = m.saturating_mul(k);
673                let bn = k.saturating_mul(n);
674                unsafe {
675                    let a_sl = sl(*a, base, a_len);
676                    let b16 = sl_typed::<u16>(*b, base, bn);
677                    let c_sl = sl_mut(*c, base, c_len);
678                    crate::blas::sgemm_f16_rhs(a_sl, b16, c_sl, m, k, n);
679                }
680            }
681
682            Thunk::SgemmT {
683                a,
684                b,
685                c,
686                m,
687                k,
688                n,
689                ta,
690                tb,
691            } => {
692                // C[m,n] = op(A) @ op(B). RowMajor cblas: lda/ldb = stored
693                // row-length of each operand → m if A is transposed else k;
694                // k if B is transposed else n. Element counts are m*k / k*n
695                // regardless of layout.
696                let (m, k, n) = (*m as usize, *k as usize, *n as usize);
697                let lda = if *ta { m } else { k };
698                let ldb = if *tb { k } else { n };
699                if trace_thunks {
700                    eprintln!(
701                        "[sgemmT] m={m} k={k} n={n} ta={ta} tb={tb} a={a}..{} b={b}..{} c={c}..{}",
702                        a + m * k * 4,
703                        b + k * n * 4,
704                        c + m * n * 4
705                    );
706                }
707                let arena_len = arena_buf.len();
708                let a_len = (m * k).min((arena_len.saturating_sub(*a)) / 4);
709                let b_len = (k * n).min((arena_len.saturating_sub(*b)) / 4);
710                let c_len = (m * n).min((arena_len.saturating_sub(*c)) / 4);
711                unsafe {
712                    let a_sl = sl(*a, base, a_len);
713                    let b_sl = sl(*b, base, b_len);
714                    let c_sl = sl_mut(*c, base, c_len);
715                    let (ap, bp) = (a_sl.as_ptr(), b_sl.as_ptr());
716                    if arena_ranges_overlap(*a, a_len, *c, c_len)
717                        || arena_ranges_overlap(*b, b_len, *c, c_len)
718                    {
719                        let mut tmp = vec![0.0f32; c_len];
720                        crate::blas::sgemm_general(
721                            ap,
722                            bp,
723                            tmp.as_mut_ptr(),
724                            m,
725                            n,
726                            k,
727                            1.0,
728                            0.0,
729                            lda,
730                            ldb,
731                            n,
732                            *ta,
733                            *tb,
734                        );
735                        c_sl.copy_from_slice(&tmp);
736                    } else {
737                        crate::blas::sgemm_general(
738                            ap,
739                            bp,
740                            c_sl.as_mut_ptr(),
741                            m,
742                            n,
743                            k,
744                            1.0,
745                            0.0,
746                            lda,
747                            ldb,
748                            n,
749                            *ta,
750                            *tb,
751                        );
752                    }
753                }
754            }
755
756            Thunk::SgdMomentum { .. } => exec_sgd_momentum(thunk, base),
757            Thunk::CgemmC64 { .. } => exec_cgemm_c64(thunk, base),
758            Thunk::DenseSolveF64 { .. } => exec_dense_solve_f64(thunk, base),
759            Thunk::DenseSolveF32 { .. } => exec_dense_solve_f32(thunk, base),
760            Thunk::CholeskyF32 { .. } => exec_cholesky(thunk, base),
761            Thunk::TriangularSolveF32 { .. } => exec_triangular_solve(thunk, base),
762            Thunk::DetF32 { .. } => exec_det(thunk, base),
763            Thunk::SortF32 { .. } => exec_sort(thunk, base),
764            Thunk::SvdF32 { .. } => exec_svd(thunk, base),
765            Thunk::QrF32 { .. } => exec_qr(thunk, base),
766            Thunk::BatchedDenseSolveF64 { .. } => exec_batched_dense_solve_f64(thunk, base),
767            Thunk::BatchedDenseSolveF32 { .. } => exec_batched_dense_solve_f32(thunk, base),
768            Thunk::BatchedDgemmF64 { .. } => exec_batched_dgemm_f64(thunk, base),
769            Thunk::BatchedSgemm {
770                a,
771                b,
772                c,
773                batch,
774                m,
775                k,
776                n,
777                a_bcast,
778                b_bcast,
779            } => {
780                let (b_, m_, k_, n_) = (*batch as usize, *m as usize, *k as usize, *n as usize);
781                if trace_thunks {
782                    eprintln!(
783                        "[batched-sgemm] batch={b_} m={m_} k={k_} n={n_} a_bcast={a_bcast} b_bcast={b_bcast} a={} b={} c={}",
784                        *a, *b, *c
785                    );
786                }
787                let a_mat = m_.saturating_mul(k_); // per-matrix element count
788                let b_mat = k_.saturating_mul(n_);
789                let c_mat = m_.saturating_mul(n_);
790                // A broadcast operand (batch dim 1) has batch stride 0 → reuse
791                // matrix 0 for every output batch; else stride by its matrix size.
792                let a_bstride = if *a_bcast { 0 } else { a_mat };
793                let b_bstride = if *b_bcast { 0 } else { b_mat };
794                let arena_len = arena_buf.len();
795                let a_cap = (arena_len.saturating_sub(*a)) / 4;
796                let b_cap = (arena_len.saturating_sub(*b)) / 4;
797                let c_cap = (arena_len.saturating_sub(*c)) / 4;
798                let a_count = if *a_bcast { 1 } else { b_ };
799                let b_count = if *b_bcast { 1 } else { b_ };
800                let a_elems = (a_count * a_mat).min(a_cap);
801                let b_elems = (b_count * b_mat).min(b_cap);
802                let c_elems = (b_ * c_mat).min(c_cap);
803                // Whole-span aliasing, decided once for the batch.
804                //
805                // A per-item check cannot see the real hazard here: with an
806                // operand stack parked inside the output slot, writing `C[0]`
807                // destroys `B[1]`, and item 1's own guard compares only its own
808                // `B[1]` against its own `C[1]` — which do not overlap. The
809                // damage lands on whichever items ran after the one that
810                // clobbered them, so it moves with the worker schedule.
811                //
812                // So the operand *stacks* are compared against the output
813                // *stack*, and if they touch at all the whole batch goes through
814                // a scratch buffer.
815                let aliased = arena_ranges_overlap(*a, a_elems, *c, c_elems)
816                    || arena_ranges_overlap(*b, b_elems, *c, c_elems);
817                if aliased {
818                    unsafe {
819                        let a_full = sl(*a, base, a_elems).to_vec();
820                        let b_full = sl(*b, base, b_elems).to_vec();
821                        let mut out = vec![0f32; c_elems];
822                        for bi in 0..b_ {
823                            let (a0, b0, c0) = (bi * a_bstride, bi * b_bstride, bi * c_mat);
824                            if a0 + a_mat > a_full.len()
825                                || b0 + b_mat > b_full.len()
826                                || c0 + c_mat > out.len()
827                            {
828                                break;
829                            }
830                            crate::blas::sgemm_auto(
831                                &a_full[a0..a0 + a_mat],
832                                &b_full[b0..b0 + b_mat],
833                                &mut out[c0..c0 + c_mat],
834                                m_,
835                                k_,
836                                n_,
837                            );
838                        }
839                        sl_mut(*c, base, c_elems).copy_from_slice(&out);
840                    }
841                    continue;
842                }
843                unsafe {
844                    let a_full = sl(*a, base, a_elems);
845                    let b_full = sl(*b, base, b_elems);
846                    let c_full = sl_mut(*c, base, c_elems);
847                    // Parallelize independent batch GEMMs with Rayon (BLAS
848                    // stays 1-thread per worker). Serial for tiny batches.
849                    if b_ >= 2 && crate::pool::num_threads() > 1 {
850                        let a_ptr = a_full.as_ptr() as usize;
851                        let b_ptr = b_full.as_ptr() as usize;
852                        let c_ptr = c_full.as_mut_ptr() as usize;
853                        let a_len = a_full.len();
854                        let b_len = b_full.len();
855                        let c_len = c_full.len();
856                        crate::pool::par_for(b_, 1, &|off, cnt| {
857                            for bi in off..off + cnt {
858                                let a0 = bi * a_bstride;
859                                let b0 = bi * b_bstride;
860                                let c0 = bi * c_mat;
861                                if a0 + a_mat > a_len || b0 + b_mat > b_len || c0 + c_mat > c_len {
862                                    break;
863                                }
864                                // SAFETY: pointers from disjoint batch slices; c
865                                // ranges don't overlap across workers.
866                                let a_slice = std::slice::from_raw_parts(
867                                    (a_ptr as *const f32).add(a0),
868                                    a_mat,
869                                );
870                                let b_slice = std::slice::from_raw_parts(
871                                    (b_ptr as *const f32).add(b0),
872                                    b_mat,
873                                );
874                                let c_slice = std::slice::from_raw_parts_mut(
875                                    (c_ptr as *mut f32).add(c0),
876                                    c_mat,
877                                );
878                                if slices_overlap(a_slice, c_slice)
879                                    || slices_overlap(b_slice, c_slice)
880                                {
881                                    let mut tmp = vec![0.0f32; c_mat];
882                                    crate::blas::sgemm(a_slice, b_slice, &mut tmp, m_, k_, n_);
883                                    c_slice.copy_from_slice(&tmp);
884                                } else {
885                                    crate::blas::sgemm(a_slice, b_slice, c_slice, m_, k_, n_);
886                                }
887                            }
888                        });
889                    } else {
890                        for bi in 0..b_ {
891                            let a0 = bi * a_bstride;
892                            let b0 = bi * b_bstride;
893                            let c0 = bi * c_mat;
894                            if a0 + a_mat > a_full.len()
895                                || b0 + b_mat > b_full.len()
896                                || c0 + c_mat > c_full.len()
897                            {
898                                break;
899                            }
900                            let a_slice = &a_full[a0..a0 + a_mat];
901                            let b_slice = &b_full[b0..b0 + b_mat];
902                            let c_slice = &mut c_full[c0..c0 + c_mat];
903                            if slices_overlap(a_slice, c_slice) || slices_overlap(b_slice, c_slice)
904                            {
905                                let mut tmp = vec![0.0f32; c_mat];
906                                crate::blas::sgemm_auto(a_slice, b_slice, &mut tmp, m_, k_, n_);
907                                c_slice.copy_from_slice(&tmp);
908                            } else {
909                                crate::blas::sgemm_auto(a_slice, b_slice, c_slice, m_, k_, n_);
910                            }
911                        }
912                    }
913                }
914            }
915
916            Thunk::Dgemm { .. } => exec_dgemm(thunk, base),
917            Thunk::TransposeF64 { .. } => exec_transpose_f64(thunk, base),
918            Thunk::ActivationF64 { .. } => exec_activation_f64(thunk, base),
919            Thunk::ReduceSumF64 { .. } => exec_reduce_sum_f64(thunk, base),
920            Thunk::CopyF64 { src, dst, len } => {
921                let mut len = *len as usize;
922                if *src == *dst || len == 0 {
923                    continue;
924                }
925                let arena_len = arena_buf.len();
926                let max_from_src = (arena_len.saturating_sub(*src)) / 8;
927                let max_from_dst = (arena_len.saturating_sub(*dst)) / 8;
928                len = len.min(max_from_src).min(max_from_dst);
929                if len == 0 {
930                    continue;
931                }
932                let byte_len = len.saturating_mul(8);
933                unsafe {
934                    std::ptr::copy(base.add(*src), base.add(*dst), byte_len);
935                }
936            }
937
938            Thunk::CopyI64 { src, dst, len } => {
939                let mut len = *len as usize;
940                if *src == *dst || len == 0 {
941                    continue;
942                }
943                let arena_len = arena_buf.len();
944                let max_from_src = (arena_len.saturating_sub(*src)) / 8;
945                let max_from_dst = (arena_len.saturating_sub(*dst)) / 8;
946                len = len.min(max_from_src).min(max_from_dst);
947                if len == 0 {
948                    continue;
949                }
950                let byte_len = len.saturating_mul(8);
951                unsafe {
952                    std::ptr::copy(base.add(*src), base.add(*dst), byte_len);
953                }
954            }
955
956            Thunk::CastF32ToI64 { src, dst, len } => {
957                let len = *len as usize;
958                if len == 0 {
959                    continue;
960                }
961                unsafe {
962                    let inp = sl(*src, base, len);
963                    let out = sl_mut_i64(*dst, base, len);
964                    // ONNX Cast float→int truncates toward zero (not round-half).
965                    for i in 0..len {
966                        out[i] = inp[i] as i64;
967                    }
968                }
969            }
970
971            Thunk::CastF32ToF64 { src, dst, len } => {
972                let len = *len as usize;
973                if len == 0 {
974                    continue;
975                }
976                unsafe {
977                    let inp = sl(*src, base, len);
978                    let out = sl_mut_f64(*dst, base, len);
979                    for i in 0..len {
980                        out[i] = inp[i] as f64;
981                    }
982                }
983            }
984
985            Thunk::CastF64ToF32 { src, dst, len } => {
986                let len = *len as usize;
987                if len == 0 {
988                    continue;
989                }
990                unsafe {
991                    let inp = sl_f64(*src, base, len);
992                    let out = sl_mut(*dst, base, len);
993                    // f64 → f32 narrowing (round-to-nearest), not a bitcast.
994                    for i in 0..len {
995                        out[i] = inp[i] as f32;
996                    }
997                }
998            }
999
1000            Thunk::CastF64ToI64 { src, dst, len } => {
1001                let len = *len as usize;
1002                if len == 0 {
1003                    continue;
1004                }
1005                unsafe {
1006                    let inp = sl_f64(*src, base, len);
1007                    let out = sl_mut_i64(*dst, base, len);
1008                    // ONNX Cast float→int truncates toward zero.
1009                    for i in 0..len {
1010                        out[i] = inp[i] as i64;
1011                    }
1012                }
1013            }
1014
1015            Thunk::CastF64ToI32 { src, dst, len } => {
1016                let len = *len as usize;
1017                if len == 0 {
1018                    continue;
1019                }
1020                unsafe {
1021                    let inp = sl_f64(*src, base, len);
1022                    let out = sl_mut_i32(*dst, base, len);
1023                    // ONNX Cast float→int truncates toward zero.
1024                    for i in 0..len {
1025                        out[i] = inp[i] as i32;
1026                    }
1027                }
1028            }
1029
1030            Thunk::CastF64ToBool { src, dst, len } => {
1031                let len = *len as usize;
1032                if len == 0 {
1033                    continue;
1034                }
1035                // f64 = 8 bytes, bool = 1 byte — copy the input out first to
1036                // avoid aliasing the arena while writing the narrower output.
1037                let inp = unsafe { sl_f64(*src, base, len).to_vec() };
1038                for i in 0..len {
1039                    arena_buf[*dst + i] = u8::from(inp[i] != 0.0);
1040                }
1041            }
1042
1043            Thunk::CastI64ToI32 { src, dst, len } => {
1044                let len = *len as usize;
1045                if len == 0 {
1046                    continue;
1047                }
1048                unsafe {
1049                    let inp = sl_i64(*src, base, len).to_vec();
1050                    let out = sl_mut_i32(*dst, base, len);
1051                    // Wrapping narrowing (matches Rust `as`, ONNX int→int).
1052                    for i in 0..len {
1053                        out[i] = inp[i] as i32;
1054                    }
1055                }
1056            }
1057
1058            Thunk::CastI64ToF64 { src, dst, len } => {
1059                let len = *len as usize;
1060                if len == 0 {
1061                    continue;
1062                }
1063                unsafe {
1064                    let inp = sl_i64(*src, base, len).to_vec();
1065                    let out = sl_mut_f64(*dst, base, len);
1066                    for i in 0..len {
1067                        out[i] = inp[i] as f64;
1068                    }
1069                }
1070            }
1071
1072            Thunk::CastI32ToF64 { src, dst, len } => {
1073                let len = *len as usize;
1074                if len == 0 {
1075                    continue;
1076                }
1077                unsafe {
1078                    let inp = sl_i32(*src, base, len).to_vec();
1079                    let out = sl_mut_f64(*dst, base, len);
1080                    for i in 0..len {
1081                        out[i] = inp[i] as f64;
1082                    }
1083                }
1084            }
1085
1086            Thunk::CastF32ToF16 { src, dst, len } => {
1087                let len = *len as usize;
1088                if len == 0 {
1089                    continue;
1090                }
1091                // f32 (4 bytes) → f16 (2 bytes). No sl_f16 accessor; copy the
1092                // input out then write the LE f16 bytes into the arena.
1093                let inp = unsafe { sl(*src, base, len).to_vec() };
1094                for i in 0..len {
1095                    let h = half::f16::from_f32(inp[i]);
1096                    arena_buf[*dst + i * 2..*dst + i * 2 + 2].copy_from_slice(&h.to_le_bytes());
1097                }
1098            }
1099
1100            Thunk::CastF16ToF32 { src, dst, len } => {
1101                let len = *len as usize;
1102                if len == 0 {
1103                    continue;
1104                }
1105                // f16 (2 bytes) → f32 (4 bytes). Read the LE f16 bytes first,
1106                // then write the widened f32 output.
1107                let bytes: Vec<u8> = arena_buf[*src..*src + len * 2].to_vec();
1108                unsafe {
1109                    let out = sl_mut(*dst, base, len);
1110                    for i in 0..len {
1111                        out[i] =
1112                            half::f16::from_le_bytes([bytes[i * 2], bytes[i * 2 + 1]]).to_f32();
1113                    }
1114                }
1115            }
1116
1117            Thunk::CastF32ToBf16 { src, dst, len } => {
1118                let len = *len as usize;
1119                if len == 0 {
1120                    continue;
1121                }
1122                let inp = unsafe { sl(*src, base, len).to_vec() };
1123                for i in 0..len {
1124                    let h = half::bf16::from_f32(inp[i]);
1125                    arena_buf[*dst + i * 2..*dst + i * 2 + 2].copy_from_slice(&h.to_le_bytes());
1126                }
1127            }
1128
1129            Thunk::CastBf16ToF32 { src, dst, len } => {
1130                let len = *len as usize;
1131                if len == 0 {
1132                    continue;
1133                }
1134                let bytes: Vec<u8> = arena_buf[*src..*src + len * 2].to_vec();
1135                unsafe {
1136                    let out = sl_mut(*dst, base, len);
1137                    for i in 0..len {
1138                        out[i] =
1139                            half::bf16::from_le_bytes([bytes[i * 2], bytes[i * 2 + 1]]).to_f32();
1140                    }
1141                }
1142            }
1143
1144            Thunk::CastF32ToI32 { src, dst, len } => {
1145                let len = *len as usize;
1146                if len == 0 {
1147                    continue;
1148                }
1149                unsafe {
1150                    let inp = sl(*src, base, len);
1151                    let out = sl_mut_i32(*dst, base, len);
1152                    // ONNX Cast float→int truncates toward zero (not round-half).
1153                    for i in 0..len {
1154                        out[i] = inp[i] as i32;
1155                    }
1156                }
1157            }
1158
1159            Thunk::CastI64ToF32 { src, dst, len } => {
1160                let len = *len as usize;
1161                if len == 0 {
1162                    continue;
1163                }
1164                unsafe {
1165                    let inp = sl_i64(*src, base, len);
1166                    let out = sl_mut(*dst, base, len);
1167                    for i in 0..len {
1168                        out[i] = inp[i] as f32;
1169                    }
1170                }
1171            }
1172
1173            Thunk::CastBoolToI32 { src, dst, len } => {
1174                let len = *len as usize;
1175                if len == 0 {
1176                    continue;
1177                }
1178                unsafe {
1179                    let inp = &arena_buf[*src..*src + len];
1180                    let out = sl_mut_i32(*dst, base, len);
1181                    for i in 0..len {
1182                        out[i] = i32::from(inp[i] != 0);
1183                    }
1184                }
1185            }
1186
1187            Thunk::CastI32ToF32 { src, dst, len } => {
1188                let len = *len as usize;
1189                if len == 0 {
1190                    continue;
1191                }
1192                unsafe {
1193                    let inp = sl_i32(*src, base, len);
1194                    let out = sl_mut(*dst, base, len);
1195                    for i in 0..len {
1196                        out[i] = inp[i] as f32;
1197                    }
1198                }
1199            }
1200
1201            Thunk::CastI32ToI64 { src, dst, len } => {
1202                let len = *len as usize;
1203                if len == 0 {
1204                    continue;
1205                }
1206                unsafe {
1207                    let inp = sl_i32(*src, base, len);
1208                    let out = sl_mut_i64(*dst, base, len);
1209                    for i in 0..len {
1210                        out[i] = inp[i] as i64;
1211                    }
1212                }
1213            }
1214
1215            Thunk::CastI32ToBool { src, dst, len } => {
1216                let len = *len as usize;
1217                if len == 0 {
1218                    continue;
1219                }
1220                // src/dst are byte offsets; i32 = 4 bytes, bool = 1 byte. Copy the
1221                // input first to avoid aliasing the arena while writing the output.
1222                let bytes: Vec<u8> = arena_buf[*src..*src + len * 4].to_vec();
1223                for i in 0..len {
1224                    let v = i32::from_le_bytes([
1225                        bytes[i * 4],
1226                        bytes[i * 4 + 1],
1227                        bytes[i * 4 + 2],
1228                        bytes[i * 4 + 3],
1229                    ]);
1230                    arena_buf[*dst + i] = u8::from(v != 0);
1231                }
1232            }
1233
1234            Thunk::CastI64ToBool { src, dst, len } => {
1235                let len = *len as usize;
1236                if len == 0 {
1237                    continue;
1238                }
1239                // i64 = 8 bytes, bool = 1 byte — copy first to avoid aliasing.
1240                let bytes: Vec<u8> = arena_buf[*src..*src + len * 8].to_vec();
1241                for i in 0..len {
1242                    let v = i64::from_le_bytes([
1243                        bytes[i * 8],
1244                        bytes[i * 8 + 1],
1245                        bytes[i * 8 + 2],
1246                        bytes[i * 8 + 3],
1247                        bytes[i * 8 + 4],
1248                        bytes[i * 8 + 5],
1249                        bytes[i * 8 + 6],
1250                        bytes[i * 8 + 7],
1251                    ]);
1252                    arena_buf[*dst + i] = u8::from(v != 0);
1253                }
1254            }
1255
1256            Thunk::CastBoolToI64 { src, dst, len } => {
1257                let len = *len as usize;
1258                if len == 0 {
1259                    continue;
1260                }
1261                let bools: Vec<u8> = arena_buf[*src..*src + len].to_vec();
1262                for i in 0..len {
1263                    let v = (bools[i] != 0) as i64;
1264                    arena_buf[*dst + i * 8..*dst + i * 8 + 8].copy_from_slice(&v.to_le_bytes());
1265                }
1266            }
1267
1268            Thunk::CastBoolToF32 { src, dst, len } => {
1269                let len = *len as usize;
1270                if len == 0 {
1271                    continue;
1272                }
1273                unsafe {
1274                    let inp = &arena_buf[*src..*src + len];
1275                    let out = sl_mut(*dst, base, len);
1276                    for i in 0..len {
1277                        out[i] = if inp[i] != 0 { 1.0 } else { 0.0 };
1278                    }
1279                }
1280            }
1281
1282            Thunk::CastGeneric {
1283                src,
1284                dst,
1285                len,
1286                src_dtype,
1287                dst_dtype,
1288            } => {
1289                let len = *len as usize;
1290                if len == 0 {
1291                    continue;
1292                }
1293                exec_cast_generic(*src, *dst, len, *src_dtype, *dst_dtype, base);
1294            }
1295
1296            Thunk::CastF32ToBool { src, dst, len } => {
1297                let len = *len as usize;
1298                if len == 0 {
1299                    continue;
1300                }
1301                unsafe {
1302                    let inp = sl(*src, base, len).to_vec();
1303                    let out = &mut arena_buf[*dst..*dst + len];
1304                    for i in 0..len {
1305                        out[i] = u8::from(inp[i] != 0.0);
1306                    }
1307                }
1308            }
1309
1310            Thunk::BinaryFullF64 { .. } => exec_binary_full_f64(thunk, base),
1311            Thunk::BinaryFullC64 { .. } => exec_binary_full_c64(thunk, base),
1312            Thunk::ComplexNormSqF32 { .. } => exec_complex_norm_sq_f32(thunk, base),
1313            Thunk::ComplexNormSqBackwardF32 { .. } => {
1314                exec_complex_norm_sq_backward_f32(thunk, base)
1315            }
1316            Thunk::ConjugateC64 { .. } => exec_conjugate_c64(thunk, base),
1317            Thunk::ActivationC64 { .. } => exec_activation_c64(thunk, base),
1318            Thunk::Scan { .. } => exec_scan(thunk, base),
1319            Thunk::ScanBackward {
1320                body_vjp,
1321                body_init,
1322                body_carry_in_off,
1323                body_x_offs,
1324                body_d_output_off,
1325                body_dcarry_out_off,
1326                outer_init_off,
1327                outer_traj_off,
1328                outer_upstream_off,
1329                outer_xs_offs,
1330                outer_dinit_off,
1331                length,
1332                carry_bytes,
1333                save_trajectory,
1334                num_checkpoints,
1335                forward_body,
1336                forward_body_init,
1337                forward_body_carry_in_off,
1338                forward_body_output_off,
1339                forward_body_x_offs,
1340                carry_elem_size,
1341            } => {
1342                // Two backward paths share the same per-iteration body
1343                // (body_vjp run + dcarry threading). The "All" path
1344                // reads the carry directly from the saved trajectory
1345                // each step. The "Recursive checkpointing" path stores
1346                // only K saved checkpoints and reconstructs intermediate
1347                // carries via Griewank-style recursive subdivision —
1348                // see [`griewank_process_segment`]. Auxiliary memory
1349                // is `O(log(segment_size) · carry_bytes)` for the
1350                // recursion stack, vs the old segment-cache scheme's
1351                // `O(segment_size · carry_bytes)`. Total recompute work
1352                // grows from `O(length)` to `O(length · log)`, which
1353                // is the canonical Griewank trade.
1354                let cb = *carry_bytes as usize;
1355                let n_steps = *length as usize;
1356                let k_total = *num_checkpoints as usize;
1357                let is_recursive = k_total != 0 && k_total != n_steps;
1358                let checkpoint_t_for_k = |k: usize| -> usize {
1359                    ((k + 1) * n_steps)
1360                        .div_ceil(k_total)
1361                        .saturating_sub(1)
1362                        .min(n_steps - 1)
1363                };
1364
1365                let mut fwd_buf: Vec<u8> = if is_recursive {
1366                    (**forward_body_init.as_ref().unwrap()).clone()
1367                } else {
1368                    Vec::new()
1369                };
1370
1371                let mut dcarry: Vec<u8> = vec![0u8; cb];
1372                if !*save_trajectory {
1373                    unsafe {
1374                        std::ptr::copy_nonoverlapping(
1375                            base.add(*outer_upstream_off),
1376                            dcarry.as_mut_ptr(),
1377                            cb,
1378                        );
1379                    }
1380                }
1381
1382                let mut body_buf: Vec<u8> = (**body_init).clone();
1383
1384                // Per-iteration backward action — shared between the
1385                // direct-trajectory (All) and Griewank (Recursive) paths.
1386                // Both feed the same body_vjp run with carry-at-t,
1387                // x_t_i, and d_output, then thread dcarry backward.
1388                let process_iter =
1389                    |t: usize, carry_in: &[u8], dcarry: &mut Vec<u8>, body_buf: &mut Vec<u8>| {
1390                        if *save_trajectory {
1391                            unsafe {
1392                                let up_off = *outer_upstream_off + t * cb;
1393                                match *carry_elem_size {
1394                                    4 => {
1395                                        let up_ptr = base.add(up_off) as *const f32;
1396                                        let dc_ptr = dcarry.as_mut_ptr() as *mut f32;
1397                                        let n_elems = cb / 4;
1398                                        for i in 0..n_elems {
1399                                            *dc_ptr.add(i) += *up_ptr.add(i);
1400                                        }
1401                                    }
1402                                    8 => {
1403                                        let up_ptr = base.add(up_off) as *const f64;
1404                                        let dc_ptr = dcarry.as_mut_ptr() as *mut f64;
1405                                        let n_elems = cb / 8;
1406                                        for i in 0..n_elems {
1407                                            *dc_ptr.add(i) += *up_ptr.add(i);
1408                                        }
1409                                    }
1410                                    other => panic!(
1411                                        "ScanBackward: unsupported carry elem size {other} \
1412                                     (only f32/f64 carries are supported today)"
1413                                    ),
1414                                }
1415                            }
1416                        }
1417                        body_buf[*body_carry_in_off..*body_carry_in_off + cb]
1418                            .copy_from_slice(carry_in);
1419                        unsafe {
1420                            for (i, body_x_off) in body_x_offs.iter().enumerate() {
1421                                let (outer_xs_off, per_step_bytes) = outer_xs_offs[i];
1422                                let psb = per_step_bytes as usize;
1423                                std::ptr::copy_nonoverlapping(
1424                                    base.add(outer_xs_off + t * psb),
1425                                    body_buf.as_mut_ptr().add(*body_x_off),
1426                                    psb,
1427                                );
1428                            }
1429                            std::ptr::copy_nonoverlapping(
1430                                dcarry.as_ptr(),
1431                                body_buf.as_mut_ptr().add(*body_d_output_off),
1432                                cb,
1433                            );
1434                        }
1435                        execute_thunks(body_vjp, body_buf);
1436                        unsafe {
1437                            std::ptr::copy_nonoverlapping(
1438                                body_buf.as_ptr().add(*body_dcarry_out_off),
1439                                dcarry.as_mut_ptr(),
1440                                cb,
1441                            );
1442                        }
1443                    };
1444
1445                if is_recursive {
1446                    // Griewank treeverse path. Process saved-checkpoint
1447                    // segments from highest-t to lowest-t; within each,
1448                    // recursive binary subdivision via
1449                    // `griewank_process_segment`. Auxiliary memory:
1450                    // O(log(seg_size) · cb) for the recursion stack
1451                    // (vs O(seg_size · cb) for the older segment-cache
1452                    // scheme); recompute work: O(seg_size · log).
1453                    let leaf_threshold = 4usize;
1454                    let fb_sched = forward_body.as_ref().unwrap();
1455                    let fb_init = forward_body_init.as_ref().unwrap().as_slice();
1456                    let mut segment_end = n_steps - 1;
1457                    for seg_k in (0..k_total).rev() {
1458                        let segment_start = if seg_k == 0 {
1459                            0
1460                        } else {
1461                            checkpoint_t_for_k(seg_k - 1) + 1
1462                        };
1463                        let mut anchor: Vec<u8> = vec![0u8; cb];
1464                        unsafe {
1465                            let src = if seg_k == 0 {
1466                                base.add(*outer_init_off)
1467                            } else {
1468                                base.add(*outer_traj_off + (seg_k - 1) * cb)
1469                            };
1470                            std::ptr::copy_nonoverlapping(src, anchor.as_mut_ptr(), cb);
1471                        }
1472                        // Closure adapter for the helper's signature
1473                        // (mutably re-borrows dcarry / body_buf each call).
1474                        let mut leaf_action = |t: usize, carry_in: &[u8]| {
1475                            process_iter(t, carry_in, &mut dcarry, &mut body_buf);
1476                        };
1477                        unsafe {
1478                            griewank_process_segment(
1479                                segment_start,
1480                                segment_end,
1481                                &anchor,
1482                                cb,
1483                                fb_sched,
1484                                fb_init,
1485                                *forward_body_carry_in_off,
1486                                *forward_body_output_off,
1487                                forward_body_x_offs,
1488                                base,
1489                                outer_xs_offs,
1490                                &mut fwd_buf,
1491                                leaf_threshold,
1492                                &mut leaf_action,
1493                            );
1494                        }
1495                        if seg_k == 0 {
1496                            break;
1497                        }
1498                        segment_end = segment_start - 1;
1499                    }
1500                } else {
1501                    // All-trajectory path: read each carry directly
1502                    // from the saved trajectory buffer.
1503                    let mut carry_buf: Vec<u8> = vec![0u8; cb];
1504                    for t in (0..n_steps).rev() {
1505                        unsafe {
1506                            let src = if t == 0 {
1507                                base.add(*outer_init_off)
1508                            } else {
1509                                base.add(*outer_traj_off + (t - 1) * cb)
1510                            };
1511                            std::ptr::copy_nonoverlapping(src, carry_buf.as_mut_ptr(), cb);
1512                        }
1513                        process_iter(t, &carry_buf, &mut dcarry, &mut body_buf);
1514                    }
1515                }
1516
1517                unsafe {
1518                    std::ptr::copy_nonoverlapping(dcarry.as_ptr(), base.add(*outer_dinit_off), cb);
1519                }
1520            }
1521
1522            Thunk::ScanBackwardXs { .. } => exec_scan_backward_xs(thunk, base),
1523            Thunk::FusedMmBiasAct { .. } => exec_fused_mm_bias_act(thunk, base),
1524            Thunk::FusedResidualLN {
1525                x,
1526                res,
1527                bias,
1528                g,
1529                b,
1530                out,
1531                rows,
1532                h,
1533                eps,
1534                has_bias,
1535            } => {
1536                let (rows, h) = (*rows as usize, *h as usize);
1537                unsafe {
1538                    let zero = &zero_bias[..h];
1539                    let bi = if *has_bias { sl(*bias, base, h) } else { zero };
1540                    let x_ptr = sl(*x, base, rows * h).as_ptr() as usize;
1541                    let r_ptr = sl(*res, base, rows * h).as_ptr() as usize;
1542                    let o_ptr = sl_mut(*out, base, rows * h).as_mut_ptr() as usize;
1543                    let bi_ptr = bi.as_ptr() as usize;
1544                    let g_ptr = sl(*g, base, h).as_ptr() as usize;
1545                    let b_ptr = sl(*b, base, h).as_ptr() as usize;
1546                    let e = *eps;
1547                    crate::pool::par_for(rows, 4, &|off, cnt| {
1548                        let xs =
1549                            std::slice::from_raw_parts((x_ptr as *const f32).add(off * h), cnt * h);
1550                        let rs =
1551                            std::slice::from_raw_parts((r_ptr as *const f32).add(off * h), cnt * h);
1552                        let os = std::slice::from_raw_parts_mut(
1553                            (o_ptr as *mut f32).add(off * h),
1554                            cnt * h,
1555                        );
1556                        let bi = std::slice::from_raw_parts(bi_ptr as *const f32, h);
1557                        let g = std::slice::from_raw_parts(g_ptr as *const f32, h);
1558                        let b = std::slice::from_raw_parts(b_ptr as *const f32, h);
1559                        crate::kernels::residual_bias_layer_norm(xs, rs, bi, g, b, os, cnt, h, e);
1560                    });
1561                }
1562            }
1563
1564            Thunk::FusedResidualRmsNorm {
1565                x,
1566                res,
1567                bias,
1568                g,
1569                b,
1570                out,
1571                rows,
1572                h,
1573                eps,
1574                has_bias,
1575            } => {
1576                let (rows, h) = (*rows as usize, *h as usize);
1577                unsafe {
1578                    let zero = &zero_bias[..h];
1579                    let bi = if *has_bias { sl(*bias, base, h) } else { zero };
1580                    let x_ptr = sl(*x, base, rows * h).as_ptr() as usize;
1581                    let r_ptr = sl(*res, base, rows * h).as_ptr() as usize;
1582                    let o_ptr = sl_mut(*out, base, rows * h).as_mut_ptr() as usize;
1583                    let bi_ptr = bi.as_ptr() as usize;
1584                    let g_ptr = sl(*g, base, h).as_ptr() as usize;
1585                    let b_ptr = sl(*b, base, h).as_ptr() as usize;
1586                    let e = *eps;
1587                    crate::pool::par_for(rows, 4, &|off, cnt| {
1588                        let xs =
1589                            std::slice::from_raw_parts((x_ptr as *const f32).add(off * h), cnt * h);
1590                        let rs =
1591                            std::slice::from_raw_parts((r_ptr as *const f32).add(off * h), cnt * h);
1592                        let os = std::slice::from_raw_parts_mut(
1593                            (o_ptr as *mut f32).add(off * h),
1594                            cnt * h,
1595                        );
1596                        let bi = std::slice::from_raw_parts(bi_ptr as *const f32, h);
1597                        let g = std::slice::from_raw_parts(g_ptr as *const f32, h);
1598                        let b = std::slice::from_raw_parts(b_ptr as *const f32, h);
1599                        crate::kernels::residual_bias_rms_norm(xs, rs, bi, g, b, os, cnt, h, e);
1600                    });
1601                }
1602            }
1603
1604            Thunk::BiasAdd { .. } => exec_bias_add(thunk, base),
1605            Thunk::BinaryFull {
1606                lhs,
1607                rhs,
1608                dst,
1609                len,
1610                lhs_len,
1611                rhs_len,
1612                op,
1613                out_dims_bcast,
1614                bcast_lhs_strides,
1615                bcast_rhs_strides,
1616                elem_bytes,
1617            } => {
1618                let len = *len as usize;
1619                let ll = (*lhs_len as usize).max(1);
1620                let rl = (*rhs_len as usize).max(1);
1621                let eb = (*elem_bytes).max(1) as usize;
1622                let arena_len = arena_buf.len();
1623                let ll = ll.min((arena_len.saturating_sub(*lhs)) / eb);
1624                let rl = rl.min((arena_len.saturating_sub(*rhs)) / eb);
1625                let len = len.min((arena_len.saturating_sub(*dst)) / eb);
1626                unsafe {
1627                    if eb == 8 {
1628                        let l = sl_i64(*lhs, base, ll);
1629                        let r = sl_i64(*rhs, base, rl);
1630                        let o = sl_mut_i64(*dst, base, len);
1631                        // Same fused-index + hoisted-op + parallel treatment as
1632                        // the f32 path below (bit-exact; elements independent).
1633                        let rank = out_dims_bcast.len();
1634                        let odb = &out_dims_bcast[..];
1635                        let lstr = &bcast_lhs_strides[..];
1636                        let rstr = &bcast_rhs_strides[..];
1637                        let idx = |i: usize| -> (usize, usize) {
1638                            if rank == 0 {
1639                                let li = if ll == 1 { 0 } else { i % ll };
1640                                let ri = if rl == 1 { 0 } else { i % rl };
1641                                (li, ri)
1642                            } else {
1643                                let mut rem = i;
1644                                let (mut li, mut ri) = (0usize, 0usize);
1645                                for ax in (0..rank).rev() {
1646                                    let sz = odb[ax] as usize;
1647                                    let c = rem % sz;
1648                                    rem /= sz;
1649                                    li += c * lstr[ax] as usize;
1650                                    ri += c * rstr[ax] as usize;
1651                                }
1652                                (li, ri)
1653                            }
1654                        };
1655                        macro_rules! bini64 {
1656                            ($f:expr) => {{
1657                                let f = $f;
1658                                if len >= 8192 {
1659                                    use rayon::prelude::*;
1660                                    o.par_iter_mut().enumerate().for_each(|(i, out)| {
1661                                        let (li, ri) = idx(i);
1662                                        *out = f(l[li], r[ri]);
1663                                    });
1664                                } else {
1665                                    for i in 0..len {
1666                                        let (li, ri) = idx(i);
1667                                        o[i] = f(l[li], r[ri]);
1668                                    }
1669                                }
1670                            }};
1671                        }
1672                        match op {
1673                            BinaryOp::Add => bini64!(|a: i64, b: i64| a.wrapping_add(b)),
1674                            BinaryOp::Sub => bini64!(|a: i64, b: i64| a.wrapping_sub(b)),
1675                            BinaryOp::Mul => bini64!(|a: i64, b: i64| a.wrapping_mul(b)),
1676                            BinaryOp::Div => {
1677                                bini64!(|a: i64, b: i64| if b == 0 { 0 } else { a / b })
1678                            }
1679                            BinaryOp::Max => bini64!(|a: i64, b: i64| a.max(b)),
1680                            BinaryOp::Min => bini64!(|a: i64, b: i64| a.min(b)),
1681                            BinaryOp::Pow => bini64!(|a: i64, b: i64| a.pow(b.max(0) as u32)),
1682                            BinaryOp::Mod => bini64!(|a: i64, b: i64| a % b),
1683                            // atan2 is float-only; unreachable on integer tensors.
1684                            BinaryOp::Atan2 => {
1685                                bini64!(|a: i64, b: i64| (a as f64).atan2(b as f64) as i64)
1686                            }
1687                            BinaryOp::BitAnd => bini64!(|a: i64, b: i64| a & b),
1688                            BinaryOp::BitOr => bini64!(|a: i64, b: i64| a | b),
1689                            BinaryOp::BitXor => bini64!(|a: i64, b: i64| a ^ b),
1690                            BinaryOp::Shl => bini64!(|a: i64, b: i64| a.wrapping_shl(b as u32)),
1691                            BinaryOp::Shr => bini64!(|a: i64, b: i64| a.wrapping_shr(b as u32)),
1692                        }
1693                    } else {
1694                        let l = sl(*lhs, base, ll);
1695                        let r = sl(*rhs, base, rl);
1696                        let o = sl_mut(*dst, base, len);
1697                        if ll == len && rl == len {
1698                            #[cfg(target_arch = "aarch64")]
1699                            if matches!(op, BinaryOp::Add | BinaryOp::Mul) {
1700                                use std::arch::aarch64::*;
1701                                let chunks = len / 4;
1702                                for c in 0..chunks {
1703                                    let off = c * 4;
1704                                    let vl = vld1q_f32(l.as_ptr().add(off));
1705                                    let vr = vld1q_f32(r.as_ptr().add(off));
1706                                    let res = match op {
1707                                        BinaryOp::Add => vaddq_f32(vl, vr),
1708                                        BinaryOp::Mul => vmulq_f32(vl, vr),
1709                                        _ => unreachable!(),
1710                                    };
1711                                    vst1q_f32(o.as_mut_ptr().add(off), res);
1712                                }
1713                                for i in (chunks * 4)..len {
1714                                    o[i] = match op {
1715                                        BinaryOp::Add => l[i] + r[i],
1716                                        BinaryOp::Mul => l[i] * r[i],
1717                                        _ => unreachable!(),
1718                                    };
1719                                }
1720                                continue;
1721                            }
1722                            // x86: contiguous same-shape Add/Mul/Sub — AVX2 when
1723                            // available, else a plain parallel loop (no N-D index walk).
1724                            #[cfg(target_arch = "x86_64")]
1725                            if matches!(op, BinaryOp::Add | BinaryOp::Mul | BinaryOp::Sub) {
1726                                let used = binary_contig_f32(l, r, o, *op);
1727                                if used {
1728                                    continue;
1729                                }
1730                            }
1731                            // Contiguous fallback for other ops / arches: skip
1732                            // broadcast index math entirely.
1733                            macro_rules! bin_contig {
1734                                ($f:expr) => {{
1735                                    let f = $f;
1736                                    if len >= 8192 {
1737                                        use rayon::prelude::*;
1738                                        o.par_iter_mut()
1739                                            .zip(l.par_iter())
1740                                            .zip(r.par_iter())
1741                                            .for_each(|((out, a), b)| *out = f(*a, *b));
1742                                    } else {
1743                                        for i in 0..len {
1744                                            o[i] = f(l[i], r[i]);
1745                                        }
1746                                    }
1747                                }};
1748                            }
1749                            match op {
1750                                BinaryOp::Add => bin_contig!(|a: f32, b: f32| a + b),
1751                                BinaryOp::Sub => bin_contig!(|a: f32, b: f32| a - b),
1752                                BinaryOp::Mul => bin_contig!(|a: f32, b: f32| a * b),
1753                                BinaryOp::Div => bin_contig!(|a: f32, b: f32| a / b),
1754                                BinaryOp::Max => bin_contig!(|a: f32, b: f32| a.max(b)),
1755                                BinaryOp::Min => bin_contig!(|a: f32, b: f32| a.min(b)),
1756                                BinaryOp::Pow => bin_contig!(|a: f32, b: f32| a.powf(b)),
1757                                BinaryOp::Mod => bin_contig!(|a: f32, b: f32| a % b),
1758                                BinaryOp::Atan2 => bin_contig!(|a: f32, b: f32| a.atan2(b)),
1759                                BinaryOp::BitAnd => {
1760                                    bin_contig!(|a: f32, b: f32| ((a as i64) & (b as i64)) as f32)
1761                                }
1762                                BinaryOp::BitOr => {
1763                                    bin_contig!(|a: f32, b: f32| ((a as i64) | (b as i64)) as f32)
1764                                }
1765                                BinaryOp::BitXor => {
1766                                    bin_contig!(|a: f32, b: f32| ((a as i64) ^ (b as i64)) as f32)
1767                                }
1768                                BinaryOp::Shl => bin_contig!(|a: f32, b: f32| (a as i64)
1769                                    .wrapping_shl(b as u32)
1770                                    as f32),
1771                                BinaryOp::Shr => bin_contig!(|a: f32, b: f32| (a as i64)
1772                                    .wrapping_shr(b as u32)
1773                                    as f32),
1774                            }
1775                            continue;
1776                        }
1777                        // Trailing-row broadcast: rhs tiles every `rl` elements
1778                        // (bias/scale along last dim). Covers empty dims and the
1779                        // common `[…, D]` rhs with leading broadcast strides 0.
1780                        if ll == len && rl > 0 && rl < len && len.is_multiple_of(rl) {
1781                            let rhs_tile = out_dims_bcast.is_empty()
1782                                || (bcast_rhs_strides.len() == out_dims_bcast.len()
1783                                    && bcast_rhs_strides.last() == Some(&1)
1784                                    && bcast_rhs_strides
1785                                        [..bcast_rhs_strides.len().saturating_sub(1)]
1786                                        .iter()
1787                                        .all(|&s| s == 0));
1788                            if rhs_tile {
1789                                let used = binary_row_bcast_f32(l, r, o, *op, rl);
1790                                if used {
1791                                    continue;
1792                                }
1793                            }
1794                        }
1795                        // Broadcast / small-operand path. This scalar loop was
1796                        // ~88% of a supertonic subgraph's CPU time. Three fixes,
1797                        // all bit-exact (each output element is independent):
1798                        //   1) fuse the coord decomposition with the stride
1799                        //      dot-product — one pass, no per-call `coords` Vec,
1800                        //      no second loop;
1801                        //   2) hoist the op-match out of the per-element loop
1802                        //      (one branch per call, not per element);
1803                        //   3) parallelize across cores for large outputs.
1804                        let rank = out_dims_bcast.len();
1805                        let odb = &out_dims_bcast[..];
1806                        let lstr = &bcast_lhs_strides[..];
1807                        let rstr = &bcast_rhs_strides[..];
1808                        let idx = |i: usize| -> (usize, usize) {
1809                            if rank == 0 {
1810                                let li = if ll == 1 { 0 } else { i % ll };
1811                                let ri = if rl == 1 { 0 } else { i % rl };
1812                                (li, ri)
1813                            } else {
1814                                let mut rem = i;
1815                                let (mut li, mut ri) = (0usize, 0usize);
1816                                for ax in (0..rank).rev() {
1817                                    let sz = odb[ax] as usize;
1818                                    let c = rem % sz;
1819                                    rem /= sz;
1820                                    li += c * lstr[ax] as usize;
1821                                    ri += c * rstr[ax] as usize;
1822                                }
1823                                (li, ri)
1824                            }
1825                        };
1826                        macro_rules! binf32 {
1827                            ($f:expr) => {{
1828                                let f = $f;
1829                                if len >= 8192 {
1830                                    use rayon::prelude::*;
1831                                    o.par_iter_mut().enumerate().for_each(|(i, out)| {
1832                                        let (li, ri) = idx(i);
1833                                        *out = f(l[li], r[ri]);
1834                                    });
1835                                } else {
1836                                    for i in 0..len {
1837                                        let (li, ri) = idx(i);
1838                                        o[i] = f(l[li], r[ri]);
1839                                    }
1840                                }
1841                            }};
1842                        }
1843                        match op {
1844                            BinaryOp::Add => binf32!(|a: f32, b: f32| a + b),
1845                            BinaryOp::Sub => binf32!(|a: f32, b: f32| a - b),
1846                            BinaryOp::Mul => binf32!(|a: f32, b: f32| a * b),
1847                            BinaryOp::Div => binf32!(|a: f32, b: f32| a / b),
1848                            BinaryOp::Max => binf32!(|a: f32, b: f32| a.max(b)),
1849                            BinaryOp::Min => binf32!(|a: f32, b: f32| a.min(b)),
1850                            BinaryOp::Pow => binf32!(|a: f32, b: f32| a.powf(b)),
1851                            BinaryOp::Mod => binf32!(|a: f32, b: f32| a % b),
1852                            BinaryOp::Atan2 => binf32!(|a: f32, b: f32| a.atan2(b)),
1853                            BinaryOp::BitAnd => {
1854                                binf32!(|a: f32, b: f32| ((a as i64) & (b as i64)) as f32)
1855                            }
1856                            BinaryOp::BitOr => {
1857                                binf32!(|a: f32, b: f32| ((a as i64) | (b as i64)) as f32)
1858                            }
1859                            BinaryOp::BitXor => {
1860                                binf32!(|a: f32, b: f32| ((a as i64) ^ (b as i64)) as f32)
1861                            }
1862                            BinaryOp::Shl => {
1863                                binf32!(|a: f32, b: f32| (a as i64).wrapping_shl(b as u32) as f32)
1864                            }
1865                            BinaryOp::Shr => {
1866                                binf32!(|a: f32, b: f32| (a as i64).wrapping_shr(b as u32) as f32)
1867                            }
1868                        }
1869                    }
1870                }
1871            }
1872
1873            Thunk::Gather { .. } => exec_gather(thunk, base),
1874            Thunk::Narrow {
1875                src,
1876                dst,
1877                outer,
1878                src_stride,
1879                dst_stride,
1880                inner,
1881                elem_bytes,
1882            } => {
1883                let (outer, ss, ds, inner, eb) = (
1884                    *outer as usize,
1885                    *src_stride as usize,
1886                    *dst_stride as usize,
1887                    *inner as usize,
1888                    *elem_bytes as usize,
1889                );
1890                let row_bytes = inner.saturating_mul(eb);
1891                let src_row_stride = ss.saturating_mul(eb);
1892                let dst_row_stride = ds.saturating_mul(eb);
1893                if trace_thunks {
1894                    eprintln!(
1895                        "[narrow] src={} dst={} outer={outer} ss={ss} ds={ds} inner={inner} eb={eb} row={row_bytes} arena={}",
1896                        *src,
1897                        *dst,
1898                        arena_buf.len()
1899                    );
1900                }
1901                if row_bytes > 0 && *src != *dst {
1902                    let arena_len = arena_buf.len();
1903                    // Parallelize independent row copies for large narrows
1904                    // (attention head splits, DiT reshape slices).
1905                    if outer >= 4
1906                        && row_bytes >= 64
1907                        && crate::pool::num_threads() > 1
1908                        && crate::pool::should_parallelize(outer.saturating_mul(row_bytes / 4))
1909                    {
1910                        let base_addr = base as usize;
1911                        let src0 = *src;
1912                        let dst0 = *dst;
1913                        crate::pool::par_for(outer, 1, &|off, cnt| {
1914                            for o in off..off + cnt {
1915                                let s_off = src0 + o * src_row_stride;
1916                                let d_off = dst0 + o * dst_row_stride;
1917                                if s_off == d_off {
1918                                    continue;
1919                                }
1920                                if s_off.saturating_add(row_bytes) > arena_len
1921                                    || d_off.saturating_add(row_bytes) > arena_len
1922                                {
1923                                    break;
1924                                }
1925                                unsafe {
1926                                    std::ptr::copy_nonoverlapping(
1927                                        (base_addr as *const u8).add(s_off),
1928                                        (base_addr as *mut u8).add(d_off),
1929                                        row_bytes,
1930                                    );
1931                                }
1932                            }
1933                        });
1934                    } else {
1935                        for o in 0..outer {
1936                            let s_off = *src + o * src_row_stride;
1937                            let d_off = *dst + o * dst_row_stride;
1938                            if s_off == d_off {
1939                                continue;
1940                            }
1941                            if s_off.saturating_add(row_bytes) > arena_len
1942                                || d_off.saturating_add(row_bytes) > arena_len
1943                            {
1944                                break;
1945                            }
1946                            unsafe {
1947                                std::ptr::copy_nonoverlapping(
1948                                    base.add(s_off),
1949                                    base.add(d_off),
1950                                    row_bytes,
1951                                );
1952                            }
1953                        }
1954                    }
1955                }
1956            }
1957
1958            Thunk::RoundHalf {
1959                src,
1960                dst,
1961                len,
1962                bf16,
1963            } => {
1964                let len = *len as usize;
1965                if len == 0 {
1966                    continue;
1967                }
1968                let inp = unsafe { sl(*src, base, len).to_vec() };
1969                let out = unsafe { sl_mut(*dst, base, len) };
1970                for (o, v) in out.iter_mut().zip(&inp) {
1971                    *o = if *bf16 {
1972                        half::bf16::from_f32(*v).to_f32()
1973                    } else {
1974                        half::f16::from_f32(*v).to_f32()
1975                    };
1976                }
1977            }
1978
1979            Thunk::Copy { src, dst, len } => {
1980                let mut len = *len as usize;
1981                if *src == *dst || len == 0 {
1982                    continue;
1983                }
1984                let arena_len = arena_buf.len();
1985                let max_from_src = (arena_len.saturating_sub(*src)) / 4;
1986                let max_from_dst = (arena_len.saturating_sub(*dst)) / 4;
1987                len = len.min(max_from_src).min(max_from_dst);
1988                if len == 0 {
1989                    continue;
1990                }
1991                let byte_len = len.saturating_mul(4);
1992                // Parallel memcpy for huge arena moves (DiT residual buffers).
1993                if len >= 262_144 && crate::pool::num_threads() > 1 {
1994                    let base_addr = base as usize;
1995                    let src0 = *src;
1996                    let dst0 = *dst;
1997                    crate::pool::par_for(len, crate::pool::chunk_floor(len), &|off, cnt| {
1998                        let n = cnt.saturating_mul(4);
1999                        unsafe {
2000                            std::ptr::copy(
2001                                (base_addr as *const u8).add(src0 + off * 4),
2002                                (base_addr as *mut u8).add(dst0 + off * 4),
2003                                n,
2004                            );
2005                        }
2006                    });
2007                } else {
2008                    unsafe {
2009                        std::ptr::copy(base.add(*src), base.add(*dst), byte_len);
2010                    }
2011                }
2012            }
2013
2014            Thunk::LayerNorm { .. } => exec_layer_norm(thunk, base),
2015            Thunk::GroupNorm { .. } => exec_group_norm(thunk, base),
2016            Thunk::BatchNormInference { .. } => exec_batch_norm_inference(thunk, base),
2017            Thunk::LayerNorm2d { .. } => exec_layer_norm2d(thunk, base),
2018            Thunk::ConvTranspose2d { .. } => exec_conv_transpose2d(thunk, base),
2019            Thunk::ResizeNearest2x { .. } => exec_resize_nearest2x(thunk, base),
2020            Thunk::Interpolate3d { .. } => exec_interpolate3d(thunk, base),
2021            Thunk::AxialRope2d { .. } => exec_axial_rope2d(thunk, base),
2022            Thunk::RmsNorm { .. } => exec_rms_norm(thunk, base),
2023            Thunk::AdaLayerNorm { .. } => exec_ada_layer_norm(thunk, base),
2024            Thunk::GatedResidual { .. } => exec_gated_residual(thunk, base),
2025            Thunk::AdaLayerNormBackward { .. } => exec_ada_layer_norm_backward(thunk, base),
2026            Thunk::GatedResidualBackward { .. } => exec_gated_residual_backward(thunk, base),
2027            Thunk::Softmax { .. } => exec_softmax(thunk, base),
2028            Thunk::Cumsum { .. } => exec_cumsum(thunk, base),
2029            Thunk::CumScan { .. } => exec_cum_scan(thunk, base),
2030            Thunk::Sample { .. } => exec_sample(thunk, base),
2031            Thunk::RngNormal {
2032                dst,
2033                len,
2034                mean,
2035                scale,
2036                key,
2037                op_seed,
2038            } => {
2039                let n = *len as usize;
2040                unsafe {
2041                    let out = sl_mut(*dst, base, n);
2042                    let opts = *schedule.rng.read().unwrap();
2043                    rlx_ir::fill_normal_like(out, *mean, *scale, opts, *key, *op_seed);
2044                }
2045            }
2046
2047            Thunk::RngUniform {
2048                dst,
2049                len,
2050                low,
2051                high,
2052                key,
2053                op_seed,
2054            } => {
2055                let n = *len as usize;
2056                unsafe {
2057                    let out = sl_mut(*dst, base, n);
2058                    let opts = *schedule.rng.read().unwrap();
2059                    rlx_ir::fill_uniform_like(out, *low, *high, opts, *key, *op_seed);
2060                }
2061            }
2062
2063            Thunk::GatedDeltaNet { .. } => exec_gated_delta_net(thunk, base),
2064            Thunk::GatedDeltaNetBackward { .. } => exec_gated_delta_net_backward(thunk, base),
2065            Thunk::Lstm { .. } => exec_lstm(thunk, base),
2066            Thunk::Gru { .. } => exec_gru(thunk, base),
2067            Thunk::Rnn { .. } => exec_rnn(thunk, base),
2068            Thunk::Mamba2 { .. } => exec_mamba2(thunk, base),
2069            Thunk::SelectiveScan { .. } => exec_selective_scan(thunk, base),
2070            Thunk::DequantMatMul { .. } => exec_dequant_mat_mul(thunk, base),
2071            Thunk::SynthMatMul { .. } => exec_synth_mat_mul(thunk, base),
2072            Thunk::SplineActivation { .. } => exec_spline_activation(thunk, base),
2073            Thunk::DequantMatMulGguf { .. } => exec_dequant_mat_mul_gguf(thunk, base),
2074            Thunk::DequantMatMulInt4 { .. } => exec_dequant_mat_mul_int4(thunk, base),
2075            Thunk::DequantMatMulFp8 { .. } => exec_dequant_mat_mul_fp8(thunk, base),
2076            Thunk::DequantMatMulNvfp4 { .. } => exec_dequant_mat_mul_nvfp4(thunk, base),
2077            Thunk::DequantMatMulMxFp4x2 { .. } => exec_dequant_mat_mul_mxfp4x2(thunk, base),
2078            Thunk::DequantMatMulMlx { .. } => exec_dequant_mat_mul_mlx(thunk, base),
2079            Thunk::ScaledMatMul { .. } => exec_scaled_mat_mul(thunk, base),
2080            Thunk::ScaledGroupedMatMul { .. } => exec_scaled_grouped_mat_mul(thunk, base),
2081            Thunk::ScaledQuantize { .. } => exec_scaled_quantize(thunk, base),
2082            Thunk::ScaledQuantScale { .. } => exec_scaled_quant_scale(thunk, base),
2083            Thunk::ScaledDequantize { .. } => exec_scaled_dequantize(thunk, base),
2084            Thunk::LoraMatMul { .. } => exec_lora_mat_mul(thunk, base),
2085            Thunk::Attention {
2086                q,
2087                k,
2088                v,
2089                mask,
2090                out,
2091                batch,
2092                seq,
2093                kv_seq,
2094                heads,
2095                head_dim,
2096                v_head_dim,
2097                mask_kind,
2098                scale,
2099                softcap,
2100                q_row_stride,
2101                k_row_stride,
2102                v_row_stride,
2103                bhsd,
2104                kv_heads,
2105            } => {
2106                let (b, q_s, k_s, nh, dh) = (
2107                    *batch as usize,
2108                    *seq as usize,
2109                    *kv_seq as usize,
2110                    *heads as usize,
2111                    *head_dim as usize,
2112                );
2113                // V/output per-head width (== dh for symmetric SDPA; MLA differs).
2114                let dh_v = *v_head_dim as usize;
2115                let nkv = (*kv_heads as usize).max(1);
2116                let group = (nh / nkv).max(1); // query heads per KV head (GQA/MQA)
2117                let hs_v = nh * dh_v; // output row stride (V-width)
2118                // For [B, H, S, D] layout each (b, h) tile is dense
2119                // contiguous; the qrs/krs/vrs strides are not used.
2120                let (qrs, krs, vrs) = if *bhsd {
2121                    (dh, dh, dh_v)
2122                } else {
2123                    (
2124                        *q_row_stride as usize,
2125                        *k_row_stride as usize,
2126                        *v_row_stride as usize,
2127                    )
2128                };
2129                let bhsd = *bhsd;
2130                let _ = (q_row_stride, k_row_stride, v_row_stride);
2131                let scale = *scale;
2132                let ss = q_s * k_s;
2133                let cfg = crate::config::RuntimeConfig::global();
2134                unsafe {
2135                    // Slice lengths cover the strided span. When Q/K/V
2136                    // alias the parent QKV (post-#46-fusion), the same
2137                    // bytes back all three slices — compiler bounds
2138                    // checks see the right size. For [B, H, S, D] the
2139                    // buffer is densely B*H*S*D elements; the row
2140                    // strides aren't used.
2141                    let q_len = if bhsd {
2142                        b * nh * q_s * dh
2143                    } else {
2144                        b * q_s * qrs
2145                    };
2146                    let k_len = if bhsd {
2147                        b * nkv * k_s * dh
2148                    } else {
2149                        b * k_s * krs
2150                    };
2151                    let v_len = if bhsd {
2152                        b * nkv * k_s * dh_v
2153                    } else {
2154                        b * k_s * vrs
2155                    };
2156                    let q_data = sl(*q, base, q_len);
2157                    let k_data = sl(*k, base, k_len);
2158                    let v_data = sl(*v, base, v_len);
2159                    let mask_data: &[f32] = match mask_kind {
2160                        rlx_ir::op::MaskKind::Custom => sl(*mask, base, b * k_s),
2161                        rlx_ir::op::MaskKind::Bias => sl(*mask, base, b * nh * q_s * k_s),
2162                        _ => &[],
2163                    };
2164                    let out_len = if bhsd {
2165                        b * nh * q_s * dh_v
2166                    } else {
2167                        b * q_s * hs_v
2168                    };
2169                    let out_data = sl_mut(*out, base, out_len);
2170
2171                    // ── [B, H, S, D] fallback ──────────────────────
2172                    // The NEON / strided-BLAS specializations below
2173                    // are written for the [B, S, H, D] layout. When
2174                    // the input is head-major ([B, H, S, D] —
2175                    // matching rlx-cuda / rlx-rocm / rlx-tpu), bypass
2176                    // them and run a simple (correct but slower)
2177                    // scalar implementation. Production-CPU inference
2178                    // graphs use [B, S, H, D] so they still hit the
2179                    // hot path; cross-backend parity tests use
2180                    // [B, H, S, D] and land here.
2181                    if bhsd {
2182                        let scores = &mut sdpa_scores[..ss];
2183                        for bi in 0..b {
2184                            for hi in 0..nh {
2185                                let kv_hi = hi / group; // GQA/MQA: shared KV head
2186                                let q_head_base = bi * nh * q_s * dh + hi * q_s * dh;
2187                                let k_head_base = bi * nkv * k_s * dh + kv_hi * k_s * dh;
2188                                // Q@K^T
2189                                for qi in 0..q_s {
2190                                    let q_base = q_head_base + qi * dh;
2191                                    for ki in 0..k_s {
2192                                        let k_base = k_head_base + ki * dh;
2193                                        let mut dot = 0f32;
2194                                        for d in 0..dh {
2195                                            dot += q_data[q_base + d] * k_data[k_base + d];
2196                                        }
2197                                        scores[qi * k_s + ki] = dot * scale;
2198                                        if matches!(mask_kind, rlx_ir::op::MaskKind::Custom)
2199                                            && !mask_data.is_empty()
2200                                            && mask_data[bi * k_s + ki] < mask_thr
2201                                        {
2202                                            scores[qi * k_s + ki] = mask_neg;
2203                                        }
2204                                    }
2205                                }
2206                                if matches!(mask_kind, rlx_ir::op::MaskKind::Bias) {
2207                                    let off = (bi * nh + hi) * q_s * k_s;
2208                                    for i in 0..q_s * k_s {
2209                                        scores[i] += mask_data[off + i];
2210                                    }
2211                                }
2212                                apply_synthetic_mask(scores, q_s, k_s, *mask_kind);
2213                                // Gemma 2 attention logit soft-cap (post-mask, pre-softmax).
2214                                if *softcap > 0.0 {
2215                                    for s in scores.iter_mut() {
2216                                        *s = *softcap * (*s / *softcap).tanh();
2217                                    }
2218                                }
2219                                crate::kernels::neon_softmax(scores, q_s, k_s);
2220                                // score @ V — V and output are dh_v-wide per head.
2221                                let o_head_base = bi * nh * q_s * dh_v + hi * q_s * dh_v;
2222                                let v_head_base = bi * nkv * k_s * dh_v + kv_hi * k_s * dh_v;
2223                                for qi in 0..q_s {
2224                                    let o_base = o_head_base + qi * dh_v;
2225                                    for d in 0..dh_v {
2226                                        out_data[o_base + d] = 0.0;
2227                                    }
2228                                    for ki in 0..k_s {
2229                                        let sc = scores[qi * k_s + ki];
2230                                        if sc > score_thr {
2231                                            let v_base = v_head_base + ki * dh_v;
2232                                            for d in 0..dh_v {
2233                                                out_data[o_base + d] += sc * v_data[v_base + d];
2234                                            }
2235                                        }
2236                                    }
2237                                }
2238                            }
2239                        }
2240                        continue;
2241                    }
2242
2243                    // ── Decode (one query row) on x86-64: a task per head ──
2244                    // Short rows take the sequential path below, which runs
2245                    // every head on one thread; long ones take the strided
2246                    // path, which without a BLAS is two loop-nest GEMMs. Both
2247                    // compute the Q·K dot for padded keys whose score the
2248                    // Custom mask then overwrites. With one query row the
2249                    // heads are independent, so split them over the pool and
2250                    // skip the dot for masked keys — the bulk of a decode
2251                    // bucket until the context fills it. Per element this is
2252                    // the sequential path's arithmetic (bit-identical there),
2253                    // with eight keys' dots run side by side in AVX lanes
2254                    // (`dots_by_key_lanes`); past the threshold it is taken
2255                    // only without a BLAS, at a 512-row bucket ~11 ms of a
2256                    // ~30 ms step on three cores.
2257                    #[cfg(target_arch = "x86_64")]
2258                    if b == 1
2259                        && q_s == 1
2260                        && nh > 1
2261                        && (k_s <= cfg.sdpa_seq_threshold || cfg!(not(rlx_cpu_blas)))
2262                        && *softcap == 0.0
2263                        && matches!(
2264                            mask_kind,
2265                            rlx_ir::op::MaskKind::Custom | rlx_ir::op::MaskKind::None
2266                        )
2267                    {
2268                        let custom = matches!(mask_kind, rlx_ir::op::MaskKind::Custom)
2269                            && !mask_data.is_empty();
2270                        let (q_d, k_d, v_d) = (q_data, k_data, v_data);
2271                        let live: Vec<usize> = (0..k_s)
2272                            .filter(|&ki| !custom || mask_data[ki] >= mask_thr)
2273                            .collect();
2274                        // 8-wide softmax chunks holding a live key (all of
2275                        // them without a mask): the rest are masked only.
2276                        let mut live_chunks: Vec<usize> = live.iter().map(|&ki| ki / 8).collect();
2277                        live_chunks.dedup();
2278                        live_chunks.retain(|&c| c < k_s / 8);
2279                        let sparse_softmax = !live.is_empty()
2280                            && std::is_x86_feature_detected!("avx2")
2281                            && std::is_x86_feature_detected!("fma");
2282                        let o_addr = out_data.as_mut_ptr() as usize;
2283                        crate::pool::par_for(nh, 1, &|off, cnt| {
2284                            let mut scores = vec![0f32; k_s];
2285                            let mut dots = vec![0f32; live.len()];
2286                            for hi in off..off + cnt {
2287                                let kv_hi = hi / group;
2288                                let q = &q_d[hi * dh..(hi + 1) * dh];
2289                                dots_by_key_lanes(q, k_d, &live, krs, kv_hi * dh, &mut dots);
2290                                scores.fill(mask_neg);
2291                                for (&ki, &dot) in live.iter().zip(&dots) {
2292                                    scores[ki] = dot * scale;
2293                                }
2294                                if sparse_softmax {
2295                                    // SAFETY: AVX2 + FMA detected above.
2296                                    crate::kernels::softmax_row_avx2_chunks(
2297                                        &mut scores,
2298                                        &live_chunks,
2299                                    );
2300                                } else {
2301                                    crate::kernels::neon_softmax(&mut scores, 1, k_s);
2302                                }
2303                                // SAFETY: each head writes only its own
2304                                // `dh_v` output columns.
2305                                let o = std::slice::from_raw_parts_mut(
2306                                    (o_addr as *mut f32).add(hi * dh_v),
2307                                    dh_v,
2308                                );
2309                                o.fill(0.0);
2310                                for &ki in &live {
2311                                    let sc = scores[ki];
2312                                    if sc > score_thr {
2313                                        let v_off = ki * vrs + kv_hi * dh_v;
2314                                        for d in 0..dh_v {
2315                                            o[d] += sc * v_d[v_off + d];
2316                                        }
2317                                    }
2318                                }
2319                            }
2320                        });
2321                        continue;
2322                    }
2323
2324                    // ── Auto-select kernel: NEON dots vs strided BLAS ───
2325                    // For tiny inputs (batch=1, short seq), per-head BLAS call
2326                    // overhead (~0.5µs × 2 calls × num_heads × num_layers)
2327                    // exceeds the NEON compute cost. Use direct strided NEON
2328                    // with zero dispatch overhead.
2329                    // For batch≥2: always BLAS + par_for (parallelism wins).
2330                    if b == 1 && q_s.max(k_s) <= cfg.sdpa_seq_threshold {
2331                        // ── Sequential NEON path (zero overhead) ──
2332                        let scores = &mut sdpa_scores[..ss];
2333                        #[cfg(target_arch = "aarch64")]
2334                        let neon_chunks = dh / 4;
2335                        #[cfg(target_arch = "aarch64")]
2336                        let neon_chunks_v = dh_v / 4;
2337
2338                        for bi in 0..b {
2339                            for hi in 0..nh {
2340                                let kv_hi = hi / group; // GQA/MQA: shared KV head
2341                                // Q@K^T via strided NEON dot products
2342                                for qi in 0..q_s {
2343                                    let q_off = bi * q_s * qrs + qi * qrs + hi * dh;
2344                                    for ki in 0..k_s {
2345                                        let k_off = bi * k_s * krs + ki * krs + kv_hi * dh;
2346                                        #[cfg(target_arch = "aarch64")]
2347                                        let mut dot;
2348                                        #[cfg(not(target_arch = "aarch64"))]
2349                                        let mut dot = 0f32;
2350                                        #[cfg(target_arch = "aarch64")]
2351                                        {
2352                                            use std::arch::aarch64::*;
2353                                            let mut acc = vdupq_n_f32(0.0);
2354                                            for c in 0..neon_chunks {
2355                                                let vq =
2356                                                    vld1q_f32(q_data.as_ptr().add(q_off + c * 4));
2357                                                let vk =
2358                                                    vld1q_f32(k_data.as_ptr().add(k_off + c * 4));
2359                                                acc = vfmaq_f32(acc, vq, vk);
2360                                            }
2361                                            dot = vaddvq_f32(acc);
2362                                            for d in (neon_chunks * 4)..dh {
2363                                                dot += q_data[q_off + d] * k_data[k_off + d];
2364                                            }
2365                                        }
2366                                        #[cfg(not(target_arch = "aarch64"))]
2367                                        for d in 0..dh {
2368                                            dot += q_data[q_off + d] * k_data[k_off + d];
2369                                        }
2370                                        scores[qi * k_s + ki] = dot * scale;
2371                                        // Inner-loop Custom mask check —
2372                                        // Causal / SlidingWindow / None
2373                                        // apply outside the loop below.
2374                                        // Skip for Bias — that mask is a
2375                                        // per-head additive tensor, not a
2376                                        // 0/1 key-padding mask.
2377                                        if matches!(mask_kind, rlx_ir::op::MaskKind::Custom)
2378                                            && !mask_data.is_empty()
2379                                            && mask_data[bi * k_s + ki] < mask_thr
2380                                        {
2381                                            scores[qi * k_s + ki] = mask_neg;
2382                                        }
2383                                    }
2384                                }
2385
2386                                if matches!(mask_kind, rlx_ir::op::MaskKind::Bias) {
2387                                    let off = (bi * nh + hi) * q_s * k_s;
2388                                    for i in 0..q_s * k_s {
2389                                        scores[i] += mask_data[off + i];
2390                                    }
2391                                }
2392                                apply_synthetic_mask(scores, q_s, k_s, *mask_kind);
2393                                crate::kernels::neon_softmax(scores, q_s, k_s);
2394
2395                                // Score@V via strided NEON accumulation (zero-copy).
2396                                // V/output are dh_v-wide per head (== dh unless MLA).
2397                                for qi in 0..q_s {
2398                                    let o_off = bi * q_s * hs_v + qi * hs_v + hi * dh_v;
2399                                    // Zero output for this head position
2400                                    for d in 0..dh_v {
2401                                        out_data[o_off + d] = 0.0;
2402                                    }
2403                                    for ki in 0..k_s {
2404                                        let sc = scores[qi * k_s + ki];
2405                                        if sc > score_thr {
2406                                            let v_off = bi * k_s * vrs + ki * vrs + kv_hi * dh_v;
2407                                            #[cfg(target_arch = "aarch64")]
2408                                            {
2409                                                use std::arch::aarch64::*;
2410                                                let vsc = vdupq_n_f32(sc);
2411                                                for c in 0..neon_chunks_v {
2412                                                    let off = c * 4;
2413                                                    let vo = vld1q_f32(
2414                                                        out_data.as_ptr().add(o_off + off),
2415                                                    );
2416                                                    let vv =
2417                                                        vld1q_f32(v_data.as_ptr().add(v_off + off));
2418                                                    vst1q_f32(
2419                                                        out_data.as_mut_ptr().add(o_off + off),
2420                                                        vfmaq_f32(vo, vsc, vv),
2421                                                    );
2422                                                }
2423                                                // scalar tail if dh_v not a multiple of 4
2424                                                for d in (neon_chunks_v * 4)..dh_v {
2425                                                    out_data[o_off + d] += sc * v_data[v_off + d];
2426                                                }
2427                                            }
2428                                            #[cfg(not(target_arch = "aarch64"))]
2429                                            for d in 0..dh_v {
2430                                                out_data[o_off + d] += sc * v_data[v_off + d];
2431                                            }
2432                                        }
2433                                    }
2434                                }
2435                            }
2436                        }
2437                    } else {
2438                        // ── Parallel strided BLAS path (high throughput) ──
2439                        let total_work = b * nh;
2440                        let q_addr = q_data.as_ptr() as usize;
2441                        let k_addr = k_data.as_ptr() as usize;
2442                        let v_addr = v_data.as_ptr() as usize;
2443                        let m_addr = mask_data.as_ptr() as usize;
2444                        let o_addr = out_data.as_mut_ptr() as usize;
2445                        let sc_addr = sdpa_scores.as_mut_ptr() as usize;
2446
2447                        crate::pool::par_for(total_work, 1, &|off, cnt| {
2448                            for idx in off..off + cnt {
2449                                let bi = idx / nh;
2450                                let hi = idx % nh;
2451                                let kv_hi = hi / group; // GQA/MQA: shared KV head
2452
2453                                let q_start = (q_addr as *const f32).add(bi * q_s * qrs + hi * dh);
2454                                let k_start =
2455                                    (k_addr as *const f32).add(bi * k_s * krs + kv_hi * dh);
2456                                // V/output are dh_v-wide per head (== dh unless MLA).
2457                                let v_start =
2458                                    (v_addr as *const f32).add(bi * k_s * vrs + kv_hi * dh_v);
2459                                let o_start = (o_addr as *mut f32).add(bi * q_s * hs_v + hi * dh_v);
2460                                let sc = std::slice::from_raw_parts_mut(
2461                                    (sc_addr as *mut f32).add(idx * ss),
2462                                    ss,
2463                                );
2464
2465                                // LDA = qrs, LDB = krs (parent row strides
2466                                // when fused; hs otherwise).
2467                                crate::blas::sgemm_general(
2468                                    q_start,
2469                                    k_start,
2470                                    sc.as_mut_ptr(),
2471                                    q_s,
2472                                    k_s,
2473                                    dh,
2474                                    scale,
2475                                    0.0,
2476                                    qrs,
2477                                    krs,
2478                                    k_s,
2479                                    false,
2480                                    true,
2481                                );
2482
2483                                match mask_kind {
2484                                    rlx_ir::op::MaskKind::Custom => {
2485                                        let mask_bi = std::slice::from_raw_parts(
2486                                            (m_addr as *const f32).add(bi * k_s),
2487                                            k_s,
2488                                        );
2489                                        for ki in 0..k_s {
2490                                            if mask_bi[ki] < mask_thr {
2491                                                for qi in 0..q_s {
2492                                                    sc[qi * k_s + ki] = mask_neg;
2493                                                }
2494                                            }
2495                                        }
2496                                    }
2497                                    rlx_ir::op::MaskKind::Bias => {
2498                                        // Per-head additive bias slice.
2499                                        let bias = std::slice::from_raw_parts(
2500                                            (m_addr as *const f32).add((bi * nh + hi) * q_s * k_s),
2501                                            q_s * k_s,
2502                                        );
2503                                        for i in 0..q_s * k_s {
2504                                            sc[i] += bias[i];
2505                                        }
2506                                    }
2507                                    _ => apply_synthetic_mask(sc, q_s, k_s, *mask_kind),
2508                                }
2509
2510                                crate::kernels::neon_softmax(sc, q_s, k_s);
2511
2512                                // score@V: N = dh_v (output/V width), LDB = vrs
2513                                // (V row stride), LDC = hs_v (output row stride).
2514                                crate::blas::sgemm_general(
2515                                    sc.as_ptr(),
2516                                    v_start,
2517                                    o_start,
2518                                    q_s,
2519                                    dh_v,
2520                                    k_s,
2521                                    1.0,
2522                                    0.0,
2523                                    k_s,
2524                                    vrs,
2525                                    hs_v,
2526                                    false,
2527                                    false,
2528                                );
2529                            }
2530                        });
2531                    }
2532                }
2533            }
2534
2535            Thunk::AttentionBackward { .. } => exec_attention_backward(thunk, base),
2536            Thunk::AttentionBackwardAll { .. } => exec_attention_backward_all(thunk, base),
2537            Thunk::ActivationInPlace { .. } => exec_activation_in_place(thunk, base),
2538            Thunk::FusedAttnBlock {
2539                hidden,
2540                qkv_w,
2541                out_w,
2542                mask,
2543                mask_kind,
2544                out,
2545                qkv_b,
2546                out_b,
2547                cos,
2548                sin,
2549                cos_len,
2550                batch,
2551                seq,
2552                hs,
2553                nh,
2554                dh,
2555                in_dim,
2556                out_dim,
2557                has_bias,
2558                has_rope,
2559                interleaved,
2560            } => {
2561                let (b, s) = (*batch as usize, *seq as usize);
2562                let (h, n_h, d_h) = (*hs as usize, *nh as usize, *dh as usize);
2563                // QKV-proj input width and out-proj output width — differ from
2564                // `h`(=hs) for MoonViT-style attention (hidden ≠ nh·dh).
2565                let (in_dim, out_dim) = (*in_dim as usize, *out_dim as usize);
2566                let interleaved = *interleaved;
2567                let m = b * s;
2568                let scale = (d_h as f32).powf(-0.5);
2569                let half = d_h / 2;
2570                // Only `Custom` consumes the per-key padding buffer; `Causal` /
2571                // `SlidingWindow` are synthesized from (qi, ki) below, and have
2572                // no mask buffer (so reading one would touch unrelated arena
2573                // bytes). q_seq == kv_seq here (guaranteed at fusion time), so
2574                // the absolute query position is just `qi`.
2575                let use_custom_mask = matches!(mask_kind, rlx_ir::op::MaskKind::Custom);
2576                unsafe {
2577                    let inp = sl(*hidden, base, m * in_dim);
2578                    let wq = sl(*qkv_w, base, in_dim * 3 * h);
2579                    let wo = sl(*out_w, base, h * out_dim);
2580                    let mk = if use_custom_mask {
2581                        sl(*mask, base, b * s)
2582                    } else {
2583                        &[]
2584                    };
2585                    let dst = sl_mut(*out, base, m * out_dim);
2586
2587                    // Stack-allocated intermediates — all fit in L1 cache for small batch
2588                    let mut qkv = vec![0f32; m * 3 * h];
2589                    let mut attn_out = vec![0f32; m * h];
2590
2591                    // 1. QKV projection: [m, in_dim] @ [in_dim, 3h] → [m, 3h]
2592                    crate::blas::par_sgemm(inp, wq, &mut qkv, m, in_dim, 3 * h);
2593                    if *has_bias {
2594                        let bias = sl(*qkv_b, base, 3 * h);
2595                        crate::blas::bias_add(&mut qkv, bias, m, 3 * h);
2596                    }
2597
2598                    // 2. Multi-head SDPA — PARALLEL across (batch, head). Each
2599                    //    task owns its scores buffer and writes a DISJOINT
2600                    //    head-slice of attn_out (`o_base` is unique per (bi,hi)),
2601                    //    so the raw-pointer writes never alias; Q/K/V/cos/sin/mask
2602                    //    are read-only. Serial per-head SDPA lost ~3× to the
2603                    //    unfused Attention thunk (which is per-head parallel) on
2604                    //    wide heads — e.g. MoonViT 12×128 @ seq 64.
2605                    #[cfg(target_arch = "aarch64")]
2606                    let neon_chunks = d_h / 4;
2607                    let qkv_ptr = qkv.as_ptr() as usize;
2608                    let out_ptr = attn_out.as_mut_ptr() as usize;
2609                    let mk_ptr = mk.as_ptr() as usize;
2610                    let mk_len = mk.len();
2611                    let base_addr = base as usize;
2612                    let (cos_off, sin_off, cl) = (*cos, *sin, *cos_len as usize);
2613                    let has_rope_c = *has_rope;
2614                    let mask_kind_c = *mask_kind;
2615                    let n_tasks = b * n_h;
2616                    crate::pool::par_for(n_tasks, 1, &|off, cnt| {
2617                        // Read-only views (multiple immutable aliases are fine).
2618                        let qkv = std::slice::from_raw_parts(qkv_ptr as *const f32, m * 3 * h);
2619                        let out = out_ptr as *mut f32;
2620                        let base = base_addr as *mut u8;
2621                        let (cos_tab, sin_tab): (&[f32], &[f32]) = if has_rope_c {
2622                            (sl(cos_off, base, cl), sl(sin_off, base, cl))
2623                        } else {
2624                            (&[], &[])
2625                        };
2626                        let mk = std::slice::from_raw_parts(mk_ptr as *const f32, mk_len);
2627                        let mut scores = vec![0f32; s * s];
2628                        for t in off..off + cnt {
2629                            let bi = t / n_h;
2630                            let hi = t % n_h;
2631                            for qi in 0..s {
2632                                let q_base = bi * s * 3 * h + qi * 3 * h + hi * d_h;
2633                                for ki in 0..s {
2634                                    let k_base = bi * s * 3 * h + ki * 3 * h + h + hi * d_h;
2635                                    let mut dot = 0f32;
2636                                    if has_rope_c {
2637                                        // Inline RoPE per pair, then dot. Pair offsets:
2638                                        //   NeoX (i, i+half)  GPT-J (2i, 2i+1).
2639                                        let q_cos = qi * half;
2640                                        let k_cos = ki * half;
2641                                        for i in 0..half {
2642                                            let (qo1, qo2, ko1, ko2) = if interleaved {
2643                                                (2 * i, 2 * i + 1, 2 * i, 2 * i + 1)
2644                                            } else {
2645                                                (i, half + i, i, half + i)
2646                                            };
2647                                            let q1 = qkv[q_base + qo1];
2648                                            let q2 = qkv[q_base + qo2];
2649                                            let k1 = qkv[k_base + ko1];
2650                                            let k2 = qkv[k_base + ko2];
2651                                            let c_q = cos_tab[q_cos + i];
2652                                            let s_q = sin_tab[q_cos + i];
2653                                            let c_k = cos_tab[k_cos + i];
2654                                            let s_k = sin_tab[k_cos + i];
2655                                            let qr1 = q1 * c_q - q2 * s_q;
2656                                            let kr1 = k1 * c_k - k2 * s_k;
2657                                            let qr2 = q2 * c_q + q1 * s_q;
2658                                            let kr2 = k2 * c_k + k1 * s_k;
2659                                            dot += qr1 * kr1 + qr2 * kr2;
2660                                        }
2661                                    } else {
2662                                        #[cfg(target_arch = "aarch64")]
2663                                        {
2664                                            use std::arch::aarch64::*;
2665                                            let mut acc = vdupq_n_f32(0.0);
2666                                            for c in 0..neon_chunks {
2667                                                let vq =
2668                                                    vld1q_f32(qkv.as_ptr().add(q_base + c * 4));
2669                                                let vk =
2670                                                    vld1q_f32(qkv.as_ptr().add(k_base + c * 4));
2671                                                acc = vfmaq_f32(acc, vq, vk);
2672                                            }
2673                                            dot = vaddvq_f32(acc);
2674                                            for d in (neon_chunks * 4)..d_h {
2675                                                dot += qkv[q_base + d] * qkv[k_base + d];
2676                                            }
2677                                        }
2678                                        #[cfg(not(target_arch = "aarch64"))]
2679                                        for d in 0..d_h {
2680                                            dot += qkv[q_base + d] * qkv[k_base + d];
2681                                        }
2682                                    }
2683                                    let mut sv = dot * scale;
2684                                    let pos_masked = match mask_kind_c {
2685                                        rlx_ir::op::MaskKind::Causal => ki > qi,
2686                                        rlx_ir::op::MaskKind::SlidingWindow(w) => {
2687                                            ki > qi || ki + w < qi
2688                                        }
2689                                        _ => false,
2690                                    };
2691                                    if pos_masked || (use_custom_mask && mk[bi * s + ki] < mask_thr)
2692                                    {
2693                                        sv = mask_neg;
2694                                    }
2695                                    scores[qi * s + ki] = sv;
2696                                }
2697                            }
2698                            crate::kernels::neon_softmax(&mut scores[..s * s], s, s);
2699                            // Score @ V accumulation (V at offset 2h in QKV).
2700                            for qi in 0..s {
2701                                let o_base = bi * s * h + qi * h + hi * d_h;
2702                                for d in 0..d_h {
2703                                    *out.add(o_base + d) = 0.0;
2704                                }
2705                                for ki in 0..s {
2706                                    let sc = scores[qi * s + ki];
2707                                    if sc > score_thr {
2708                                        let v_base = bi * s * 3 * h + ki * 3 * h + 2 * h + hi * d_h;
2709                                        #[cfg(target_arch = "aarch64")]
2710                                        {
2711                                            use std::arch::aarch64::*;
2712                                            let vsc = vdupq_n_f32(sc);
2713                                            for c in 0..neon_chunks {
2714                                                let o = c * 4;
2715                                                let vo = vld1q_f32(out.add(o_base + o));
2716                                                let vv = vld1q_f32(qkv.as_ptr().add(v_base + o));
2717                                                vst1q_f32(
2718                                                    out.add(o_base + o),
2719                                                    vfmaq_f32(vo, vsc, vv),
2720                                                );
2721                                            }
2722                                        }
2723                                        #[cfg(not(target_arch = "aarch64"))]
2724                                        for d in 0..d_h {
2725                                            *out.add(o_base + d) += sc * qkv[v_base + d];
2726                                        }
2727                                    }
2728                                }
2729                            }
2730                        }
2731                    });
2732
2733                    // 3. Output projection: [m, h] @ [h, out_dim] → dst
2734                    crate::blas::par_sgemm(&attn_out, wo, dst, m, h, out_dim);
2735                    if *has_bias {
2736                        let bias = sl(*out_b, base, out_dim);
2737                        crate::blas::bias_add(dst, bias, m, out_dim);
2738                    }
2739                }
2740            }
2741
2742            Thunk::Rope { .. } => exec_rope(thunk, base),
2743            Thunk::FusedBertLayer {
2744                hidden,
2745                qkv_w,
2746                qkv_b,
2747                out_w,
2748                out_b,
2749                mask,
2750                ln1_g,
2751                ln1_b,
2752                eps1,
2753                fc1_w,
2754                fc1_b,
2755                fc2_w,
2756                fc2_b,
2757                ln2_g,
2758                ln2_b,
2759                eps2,
2760                out,
2761                batch,
2762                seq,
2763                hs,
2764                nh,
2765                dh,
2766                int_dim,
2767            } => {
2768                let (b, s, h, n_h, d_h) = (
2769                    *batch as usize,
2770                    *seq as usize,
2771                    *hs as usize,
2772                    *nh as usize,
2773                    *dh as usize,
2774                );
2775                let m = b * s;
2776                let id = *int_dim as usize;
2777                let scale = (d_h as f32).powf(-0.5);
2778                let _half = d_h / 2;
2779                #[cfg(target_arch = "aarch64")]
2780                let neon_chunks = d_h / 4;
2781                unsafe {
2782                    let inp = sl(*hidden, base, m * h);
2783                    let dst = sl_mut(*out, base, m * h);
2784                    let mk = sl(*mask, base, b * s);
2785
2786                    // Pre-allocated buffers (zero malloc per layer — allocated once before thunk loop)
2787                    let qkv = std::slice::from_raw_parts_mut(fl_qkv.as_mut_ptr(), m * 3 * h);
2788                    let attn = std::slice::from_raw_parts_mut(fl_attn.as_mut_ptr(), m * h);
2789                    let res = std::slice::from_raw_parts_mut(fl_res.as_mut_ptr(), m * h);
2790                    let normed = std::slice::from_raw_parts_mut(fl_normed.as_mut_ptr(), m * h);
2791                    let ffn = std::slice::from_raw_parts_mut(fl_ffn.as_mut_ptr(), m * id);
2792                    let sc = std::slice::from_raw_parts_mut(fl_sc.as_mut_ptr(), s * s);
2793
2794                    // QKV (parallelized across cores — multiple AMX coprocessors)
2795                    crate::blas::par_sgemm_bias(
2796                        inp,
2797                        sl(*qkv_w, base, h * 3 * h),
2798                        sl(*qkv_b, base, 3 * h),
2799                        qkv,
2800                        m,
2801                        h,
2802                        3 * h,
2803                    );
2804
2805                    // SDPA per head (sequential NEON, inline — zero overhead)
2806                    for bi in 0..b {
2807                        for hi in 0..n_h {
2808                            for qi in 0..s {
2809                                for ki in 0..s {
2810                                    let q_base = bi * s * 3 * h + qi * 3 * h + hi * d_h;
2811                                    let k_base = bi * s * 3 * h + ki * 3 * h + h + hi * d_h;
2812                                    #[cfg(target_arch = "aarch64")]
2813                                    let dot;
2814                                    #[cfg(not(target_arch = "aarch64"))]
2815                                    let mut dot = 0f32;
2816                                    #[cfg(target_arch = "aarch64")]
2817                                    {
2818                                        use std::arch::aarch64::*;
2819                                        let mut acc = vdupq_n_f32(0.0);
2820                                        for c in 0..neon_chunks {
2821                                            acc = vfmaq_f32(
2822                                                acc,
2823                                                vld1q_f32(qkv.as_ptr().add(q_base + c * 4)),
2824                                                vld1q_f32(qkv.as_ptr().add(k_base + c * 4)),
2825                                            );
2826                                        }
2827                                        dot = vaddvq_f32(acc);
2828                                    }
2829                                    #[cfg(not(target_arch = "aarch64"))]
2830                                    for d in 0..d_h {
2831                                        dot += qkv[q_base + d] * qkv[k_base + d];
2832                                    }
2833                                    sc[qi * s + ki] = dot * scale;
2834                                    if mk[bi * s + ki] < mask_thr {
2835                                        sc[qi * s + ki] = mask_neg;
2836                                    }
2837                                }
2838                            }
2839                            crate::kernels::neon_softmax(&mut sc[..s * s], s, s);
2840                            for qi in 0..s {
2841                                let o = bi * s * h + qi * h + hi * d_h;
2842                                for d in 0..d_h {
2843                                    attn[o + d] = 0.0;
2844                                }
2845                                for ki in 0..s {
2846                                    let w = sc[qi * s + ki];
2847                                    if w > score_thr {
2848                                        let v = bi * s * 3 * h + ki * 3 * h + 2 * h + hi * d_h;
2849                                        #[cfg(target_arch = "aarch64")]
2850                                        {
2851                                            use std::arch::aarch64::*;
2852                                            let vw = vdupq_n_f32(w);
2853                                            for c in 0..neon_chunks {
2854                                                let off = c * 4;
2855                                                vst1q_f32(
2856                                                    attn.as_mut_ptr().add(o + off),
2857                                                    vfmaq_f32(
2858                                                        vld1q_f32(attn.as_ptr().add(o + off)),
2859                                                        vw,
2860                                                        vld1q_f32(qkv.as_ptr().add(v + off)),
2861                                                    ),
2862                                                );
2863                                            }
2864                                        }
2865                                        #[cfg(not(target_arch = "aarch64"))]
2866                                        for d in 0..d_h {
2867                                            attn[o + d] += w * qkv[v + d];
2868                                        }
2869                                    }
2870                                }
2871                            }
2872                        }
2873                    }
2874
2875                    // Out proj (sgemm + bias fused) + residual add with NEON
2876                    crate::blas::sgemm_bias(
2877                        attn,
2878                        sl(*out_w, base, h * h),
2879                        sl(*out_b, base, h),
2880                        res,
2881                        m,
2882                        h,
2883                        h,
2884                    );
2885                    #[cfg(target_arch = "aarch64")]
2886                    {
2887                        use std::arch::aarch64::*;
2888                        let chunks_h = (m * h) / 4;
2889                        for c in 0..chunks_h {
2890                            let off = c * 4;
2891                            vst1q_f32(
2892                                res.as_mut_ptr().add(off),
2893                                vaddq_f32(
2894                                    vld1q_f32(res.as_ptr().add(off)),
2895                                    vld1q_f32(inp.as_ptr().add(off)),
2896                                ),
2897                            );
2898                        }
2899                        for i in (chunks_h * 4)..(m * h) {
2900                            res[i] += inp[i];
2901                        }
2902                    }
2903                    #[cfg(not(target_arch = "aarch64"))]
2904                    for i in 0..m * h {
2905                        res[i] += inp[i];
2906                    }
2907
2908                    // LN1 (fused residual already done above — just normalize)
2909                    let g1 = sl(*ln1_g, base, h);
2910                    let b1 = sl(*ln1_b, base, h);
2911                    crate::kernels::layer_norm_rows(&res[..], g1, b1, &mut normed[..], m, h, *eps1);
2912
2913                    // FFN: fc1 (parallel across cores) + GELU
2914                    crate::blas::par_sgemm_bias(
2915                        normed,
2916                        sl(*fc1_w, base, h * id),
2917                        sl(*fc1_b, base, id),
2918                        ffn,
2919                        m,
2920                        h,
2921                        id,
2922                    );
2923                    crate::kernels::par_gelu_inplace(ffn);
2924
2925                    // fc2 + bias (parallel across cores) + residual with NEON
2926                    crate::blas::par_sgemm_bias(
2927                        ffn,
2928                        sl(*fc2_w, base, id * h),
2929                        sl(*fc2_b, base, h),
2930                        res,
2931                        m,
2932                        id,
2933                        h,
2934                    );
2935                    #[cfg(target_arch = "aarch64")]
2936                    {
2937                        use std::arch::aarch64::*;
2938                        let chunks_h = (m * h) / 4;
2939                        for c in 0..chunks_h {
2940                            let off = c * 4;
2941                            vst1q_f32(
2942                                res.as_mut_ptr().add(off),
2943                                vaddq_f32(
2944                                    vld1q_f32(res.as_ptr().add(off)),
2945                                    vld1q_f32(normed.as_ptr().add(off)),
2946                                ),
2947                            );
2948                        }
2949                        for i in (chunks_h * 4)..(m * h) {
2950                            res[i] += normed[i];
2951                        }
2952                    }
2953                    #[cfg(not(target_arch = "aarch64"))]
2954                    for i in 0..m * h {
2955                        res[i] += normed[i];
2956                    }
2957
2958                    // LN2 → output
2959                    let g2 = sl(*ln2_g, base, h);
2960                    let b2 = sl(*ln2_b, base, h);
2961                    crate::kernels::layer_norm_rows(&res[..], g2, b2, &mut dst[..], m, h, *eps2);
2962                }
2963            }
2964
2965            Thunk::FusedNomicLayer {
2966                hidden,
2967                qkv_w,
2968                out_w,
2969                mask,
2970                cos,
2971                sin,
2972                cos_len,
2973                ln1_g,
2974                ln1_b,
2975                eps1,
2976                fc11_w,
2977                fc12_w: _,
2978                fc2_w,
2979                ln2_g,
2980                ln2_b,
2981                eps2,
2982                out,
2983                batch,
2984                seq,
2985                hs,
2986                nh,
2987                dh,
2988                int_dim,
2989                interleaved,
2990            } => {
2991                let interleaved = *interleaved;
2992                let (b, s, h, n_h, d_h) = (
2993                    *batch as usize,
2994                    *seq as usize,
2995                    *hs as usize,
2996                    *nh as usize,
2997                    *dh as usize,
2998                );
2999                let m = b * s;
3000                let id = *int_dim as usize;
3001                let scale = (d_h as f32).powf(-0.5);
3002                let half_dh = d_h / 2;
3003                #[cfg(target_arch = "aarch64")]
3004                let neon_chunks = d_h / 4;
3005                unsafe {
3006                    let inp = sl(*hidden, base, m * h);
3007                    let dst = sl_mut(*out, base, m * h);
3008                    let mk = sl(*mask, base, b * s);
3009                    let cos_tab = sl(*cos, base, *cos_len as usize);
3010                    let sin_tab = sl(*sin, base, *cos_len as usize);
3011                    // fc11_w is the fused [h, 2*int_dim] weight (fc11 || fc12 concatenated)
3012                    let fused_fc_w = sl(*fc11_w, base, h * 2 * id);
3013
3014                    let mut qkv = vec![0f32; m * 3 * h];
3015                    let mut attn = vec![0f32; m * h];
3016                    let mut res = vec![0f32; m * h];
3017                    let mut normed = vec![0f32; m * h];
3018                    let mut ffn_concat = vec![0f32; m * 2 * id]; // fc11||fc12 output
3019                    let mut sc = vec![0f32; s * s];
3020
3021                    // QKV (no bias)
3022                    crate::blas::sgemm(inp, sl(*qkv_w, base, h * 3 * h), &mut qkv, m, h, 3 * h);
3023
3024                    // SDPA with inline RoPE
3025                    for bi in 0..b {
3026                        for hi in 0..n_h {
3027                            for qi in 0..s {
3028                                for ki in 0..s {
3029                                    let q_base = bi * s * 3 * h + qi * 3 * h + hi * d_h;
3030                                    let k_base = bi * s * 3 * h + ki * 3 * h + h + hi * d_h;
3031                                    let mut dot = 0f32;
3032                                    for i in 0..half_dh {
3033                                        // NeoX pairs (i, i+half); GPT-J pairs (2i, 2i+1).
3034                                        let (o1, o2) = if interleaved {
3035                                            (2 * i, 2 * i + 1)
3036                                        } else {
3037                                            (i, half_dh + i)
3038                                        };
3039                                        let q1 = qkv[q_base + o1];
3040                                        let q2 = qkv[q_base + o2];
3041                                        let k1 = qkv[k_base + o1];
3042                                        let k2 = qkv[k_base + o2];
3043                                        let cq = cos_tab[qi * half_dh + i];
3044                                        let sq = sin_tab[qi * half_dh + i];
3045                                        let ck = cos_tab[ki * half_dh + i];
3046                                        let sk = sin_tab[ki * half_dh + i];
3047                                        dot += (q1 * cq - q2 * sq) * (k1 * ck - k2 * sk)
3048                                            + (q2 * cq + q1 * sq) * (k2 * ck + k1 * sk);
3049                                    }
3050                                    sc[qi * s + ki] = dot * scale;
3051                                    if mk[bi * s + ki] < mask_thr {
3052                                        sc[qi * s + ki] = mask_neg;
3053                                    }
3054                                }
3055                            }
3056                            crate::kernels::neon_softmax(&mut sc[..s * s], s, s);
3057                            for qi in 0..s {
3058                                let o = bi * s * h + qi * h + hi * d_h;
3059                                for d in 0..d_h {
3060                                    attn[o + d] = 0.0;
3061                                }
3062                                for ki in 0..s {
3063                                    let w = sc[qi * s + ki];
3064                                    if w > score_thr {
3065                                        let v = bi * s * 3 * h + ki * 3 * h + 2 * h + hi * d_h;
3066                                        #[cfg(target_arch = "aarch64")]
3067                                        {
3068                                            use std::arch::aarch64::*;
3069                                            let vw = vdupq_n_f32(w);
3070                                            for c in 0..neon_chunks {
3071                                                let off = c * 4;
3072                                                vst1q_f32(
3073                                                    attn.as_mut_ptr().add(o + off),
3074                                                    vfmaq_f32(
3075                                                        vld1q_f32(attn.as_ptr().add(o + off)),
3076                                                        vw,
3077                                                        vld1q_f32(qkv.as_ptr().add(v + off)),
3078                                                    ),
3079                                                );
3080                                            }
3081                                        }
3082                                        #[cfg(not(target_arch = "aarch64"))]
3083                                        for d in 0..d_h {
3084                                            attn[o + d] += w * qkv[v + d];
3085                                        }
3086                                    }
3087                                }
3088                            }
3089                        }
3090                    }
3091
3092                    // Out proj (no bias) + residual
3093                    crate::blas::sgemm(&attn, sl(*out_w, base, h * h), &mut res, m, h, h);
3094                    for i in 0..m * h {
3095                        res[i] += inp[i];
3096                    }
3097
3098                    // LN1
3099                    let g1 = sl(*ln1_g, base, h);
3100                    let b1 = sl(*ln1_b, base, h);
3101                    crate::kernels::layer_norm_rows(&res[..], g1, b1, &mut normed[..], m, h, *eps1);
3102
3103                    // SwiGLU: fused fc11+fc12 sgemm, then split, silu, mul
3104                    crate::blas::sgemm(&normed, fused_fc_w, &mut ffn_concat, m, h, 2 * id);
3105                    // Split: first id cols = fc11 (up), second id cols = fc12 (gate)
3106                    // SiLU on gate, then multiply up * gate → store in up region
3107                    for row in 0..m {
3108                        let bo = row * 2 * id;
3109                        // SiLU in-place on gate portion
3110                        for j in 0..id {
3111                            let x = ffn_concat[bo + id + j];
3112                            ffn_concat[bo + id + j] = x / (1.0 + (-x).exp());
3113                        }
3114                        // Multiply: up[j] *= gate[j]
3115                        for j in 0..id {
3116                            ffn_concat[bo + j] *= ffn_concat[bo + id + j];
3117                        }
3118                    }
3119
3120                    // fc2 (no bias) + residual. The up*silu(gate) product lives in
3121                    // the FIRST `id` cols of each 2*id-wide ffn_concat row; gather
3122                    // it contiguous and run the SAME `sgemm` dispatch the unfused
3123                    // path uses (a strided `sgemm_general` here would force the
3124                    // BLAS/scalar path and diverge from the unfused NEON sgemm).
3125                    let mut swiglu_contig = vec![0f32; m * id];
3126                    for row in 0..m {
3127                        let bo = row * 2 * id;
3128                        swiglu_contig[row * id..(row + 1) * id]
3129                            .copy_from_slice(&ffn_concat[bo..bo + id]);
3130                    }
3131                    crate::blas::sgemm(
3132                        &swiglu_contig,
3133                        sl(*fc2_w, base, id * h),
3134                        &mut res,
3135                        m,
3136                        id,
3137                        h,
3138                    );
3139                    for i in 0..m * h {
3140                        res[i] += normed[i];
3141                    }
3142
3143                    // LN2 → output
3144                    let g2 = sl(*ln2_g, base, h);
3145                    let b2 = sl(*ln2_b, base, h);
3146                    crate::kernels::layer_norm_rows(&res[..], g2, b2, &mut dst[..], m, h, *eps2);
3147                }
3148            }
3149
3150            Thunk::FusedSwiGLU { .. } => exec_fused_swi_g_l_u(thunk, base),
3151            Thunk::Concat { .. } => exec_concat(thunk, base),
3152            Thunk::KvAppend { .. } => exec_kv_append(thunk, base),
3153            Thunk::ConcatF64 { .. } => exec_concat_f64(thunk, base),
3154            Thunk::Compare {
3155                lhs,
3156                rhs,
3157                dst,
3158                len,
3159                op,
3160                inputs_i64,
3161                inputs_elem_bytes,
3162                dst_elem_bytes,
3163                lhs_scalar,
3164                rhs_scalar,
3165                lhs_len,
3166                rhs_len,
3167                out_dims_bcast,
3168                bcast_lhs_strides,
3169                bcast_rhs_strides,
3170            } => {
3171                let len = *len as usize;
3172                let arena_len = arena_buf.len();
3173                let elem = (*inputs_elem_bytes).max(1) as usize;
3174                let dst_eb = (*dst_elem_bytes).max(1) as usize;
3175                let max_l = (arena_len.saturating_sub(*lhs)) / elem;
3176                let max_r = (arena_len.saturating_sub(*rhs)) / elem;
3177                let max_d = (arena_len.saturating_sub(*dst)) / dst_eb;
3178                if !out_dims_bcast.is_empty() {
3179                    // Shape-aware broadcast (partial size-1 axes, e.g. `[.,L,1]`
3180                    // vs `[.,L,L]`). Read each operand at its TRUE element count
3181                    // and index via row-major coords · strides — the flat path
3182                    // below would read past a partial operand into adjacent
3183                    // arena memory. Mirrors `BinaryFull`'s broadcast loop.
3184                    let len = len.min(max_d);
3185                    let l_read = (*lhs_len as usize).min(max_l).max(1);
3186                    let r_read = (*rhs_len as usize).min(max_r).max(1);
3187                    let rank = out_dims_bcast.len();
3188                    let bidx = |i: usize| -> (usize, usize) {
3189                        let mut rem = i;
3190                        let (mut li, mut ri) = (0usize, 0usize);
3191                        for ax in (0..rank).rev() {
3192                            let sz = out_dims_bcast[ax] as usize;
3193                            let c = rem % sz;
3194                            rem /= sz;
3195                            li += c * bcast_lhs_strides[ax] as usize;
3196                            ri += c * bcast_rhs_strides[ax] as usize;
3197                        }
3198                        (li, ri)
3199                    };
3200                    macro_rules! do_cmp {
3201                        ($a:expr, $b:expr) => {
3202                            match op {
3203                                CmpOp::Eq => $a == $b,
3204                                CmpOp::Ne => $a != $b,
3205                                CmpOp::Lt => $a < $b,
3206                                CmpOp::Le => $a <= $b,
3207                                CmpOp::Gt => $a > $b,
3208                                CmpOp::Ge => $a >= $b,
3209                            }
3210                        };
3211                    }
3212                    macro_rules! put {
3213                        ($i:expr, $v:expr) => {
3214                            if *dst_elem_bytes == 1 {
3215                                arena_buf[*dst + $i] = u8::from($v);
3216                            } else {
3217                                let o = sl_mut(*dst, base, len);
3218                                o[$i] = if $v { 1.0 } else { 0.0 };
3219                            }
3220                        };
3221                    }
3222                    if elem == 1 {
3223                        let l = arena_buf[*lhs..*lhs + l_read].to_vec();
3224                        let r = arena_buf[*rhs..*rhs + r_read].to_vec();
3225                        for i in 0..len {
3226                            let (li, ri) = bidx(i);
3227                            let v = do_cmp!(l[li], r[ri]);
3228                            unsafe { put!(i, v) }
3229                        }
3230                    } else if *inputs_i64 != 0 {
3231                        unsafe {
3232                            let l = sl_i64(*lhs, base, l_read);
3233                            let r = sl_i64(*rhs, base, r_read);
3234                            for i in 0..len {
3235                                let (li, ri) = bidx(i);
3236                                let v = do_cmp!(l[li], r[ri]);
3237                                put!(i, v)
3238                            }
3239                        }
3240                    } else {
3241                        unsafe {
3242                            let l = sl(*lhs, base, l_read);
3243                            let r = sl(*rhs, base, r_read);
3244                            for i in 0..len {
3245                                let (li, ri) = bidx(i);
3246                                let v = do_cmp!(l[li], r[ri]);
3247                                put!(i, v)
3248                            }
3249                        }
3250                    }
3251                } else {
3252                    let l_n = if *lhs_scalar { 1 } else { len };
3253                    let r_n = if *rhs_scalar { 1 } else { len };
3254                    // Keep full `len` when broadcasting scalars — only the
3255                    // non-scalar operands (and dst) may shrink the loop.
3256                    let mut len = len.min(max_d);
3257                    if *lhs_scalar {
3258                        if max_l < 1 {
3259                            len = 0;
3260                        }
3261                    } else {
3262                        len = len.min(max_l);
3263                    }
3264                    if *rhs_scalar {
3265                        if max_r < 1 {
3266                            len = 0;
3267                        }
3268                    } else {
3269                        len = len.min(max_r);
3270                    }
3271                    if trace_thunks && len > 0 {
3272                        eprintln!(
3273                            "[compare] len={len} lhs={} rhs={} dst={} ls={} rs={}",
3274                            *lhs, *rhs, *dst, *lhs_scalar, *rhs_scalar
3275                        );
3276                    }
3277                    if elem == 1 {
3278                        let l = arena_buf[*lhs..*lhs + l_n.min(max_l).max(1)].to_vec();
3279                        let r = arena_buf[*rhs..*rhs + r_n.min(max_r).max(1)].to_vec();
3280                        for i in 0..len {
3281                            let li = if *lhs_scalar { 0 } else { i };
3282                            let ri = if *rhs_scalar { 0 } else { i };
3283                            let v = match op {
3284                                CmpOp::Eq => l[li] == r[ri],
3285                                CmpOp::Ne => l[li] != r[ri],
3286                                CmpOp::Lt => l[li] < r[ri],
3287                                CmpOp::Le => l[li] <= r[ri],
3288                                CmpOp::Gt => l[li] > r[ri],
3289                                CmpOp::Ge => l[li] >= r[ri],
3290                            };
3291                            if *dst_elem_bytes == 1 {
3292                                arena_buf[*dst + i] = u8::from(v);
3293                            } else {
3294                                unsafe {
3295                                    let o = sl_mut(*dst, base, len);
3296                                    o[i] = if v { 1.0 } else { 0.0 };
3297                                }
3298                            }
3299                        }
3300                    } else if *inputs_i64 != 0 {
3301                        unsafe {
3302                            let l = sl_i64(*lhs, base, l_n.min(max_l).max(1));
3303                            let r = sl_i64(*rhs, base, r_n.min(max_r).max(1));
3304                            for i in 0..len {
3305                                let li = if *lhs_scalar { 0 } else { i };
3306                                let ri = if *rhs_scalar { 0 } else { i };
3307                                let v = match op {
3308                                    CmpOp::Eq => l[li] == r[ri],
3309                                    CmpOp::Ne => l[li] != r[ri],
3310                                    CmpOp::Lt => l[li] < r[ri],
3311                                    CmpOp::Le => l[li] <= r[ri],
3312                                    CmpOp::Gt => l[li] > r[ri],
3313                                    CmpOp::Ge => l[li] >= r[ri],
3314                                };
3315                                if *dst_elem_bytes == 1 {
3316                                    arena_buf[*dst + i] = u8::from(v);
3317                                } else {
3318                                    let o = sl_mut(*dst, base, len);
3319                                    o[i] = if v { 1.0 } else { 0.0 };
3320                                }
3321                            }
3322                        }
3323                    } else {
3324                        unsafe {
3325                            let l = sl(*lhs, base, l_n.min(max_l).max(1));
3326                            let r = sl(*rhs, base, r_n.min(max_r).max(1));
3327                            for i in 0..len {
3328                                let li = if *lhs_scalar { 0 } else { i };
3329                                let ri = if *rhs_scalar { 0 } else { i };
3330                                let v = match op {
3331                                    CmpOp::Eq => l[li] == r[ri],
3332                                    CmpOp::Ne => l[li] != r[ri],
3333                                    CmpOp::Lt => l[li] < r[ri],
3334                                    CmpOp::Le => l[li] <= r[ri],
3335                                    CmpOp::Gt => l[li] > r[ri],
3336                                    CmpOp::Ge => l[li] >= r[ri],
3337                                };
3338                                if *dst_elem_bytes == 1 {
3339                                    arena_buf[*dst + i] = u8::from(v);
3340                                } else {
3341                                    let o = sl_mut(*dst, base, len);
3342                                    o[i] = if v { 1.0 } else { 0.0 };
3343                                }
3344                            }
3345                        }
3346                    }
3347                }
3348            }
3349
3350            Thunk::Where {
3351                cond,
3352                on_true,
3353                on_false,
3354                dst,
3355                len,
3356                elem_bytes,
3357                cond_elem_bytes,
3358                cond_scalar,
3359                true_scalar,
3360                false_scalar,
3361            } => {
3362                let len = *len as usize;
3363                let eb = *elem_bytes as usize;
3364                let cond_eb = (*cond_elem_bytes).max(1) as usize;
3365                let arena_len = arena_buf.len();
3366                let c_n = if *cond_scalar { 1 } else { len };
3367                let t_n = if *true_scalar { 1 } else { len };
3368                let f_n = if *false_scalar { 1 } else { len };
3369                let max_c = (arena_len.saturating_sub(*cond)) / cond_eb;
3370                let max_t = (arena_len.saturating_sub(*on_true)) / eb;
3371                let max_f = (arena_len.saturating_sub(*on_false)) / eb;
3372                let max_d = (arena_len.saturating_sub(*dst)) / eb;
3373                let mut len = len.min(max_d);
3374                if *cond_scalar {
3375                    if max_c < 1 {
3376                        len = 0;
3377                    }
3378                } else {
3379                    len = len.min(max_c);
3380                }
3381                if *true_scalar {
3382                    if max_t < 1 {
3383                        len = 0;
3384                    }
3385                } else {
3386                    len = len.min(max_t);
3387                }
3388                if *false_scalar {
3389                    if max_f < 1 {
3390                        len = 0;
3391                    }
3392                } else {
3393                    len = len.min(max_f);
3394                }
3395                unsafe {
3396                    if *elem_bytes == 8 {
3397                        let t = sl_i64(*on_true, base, t_n.min(max_t).max(1));
3398                        let e = sl_i64(*on_false, base, f_n.min(max_f).max(1));
3399                        let o = sl_mut_i64(*dst, base, len);
3400                        if *cond_elem_bytes == 1 {
3401                            let c = &arena_buf[*cond..*cond + c_n.min(max_c).max(1)];
3402                            for i in 0..len {
3403                                let ci = if *cond_scalar { 0 } else { i };
3404                                let ti = if *true_scalar { 0 } else { i };
3405                                let ei = if *false_scalar { 0 } else { i };
3406                                o[i] = if c[ci] != 0 { t[ti] } else { e[ei] };
3407                            }
3408                        } else if *cond_elem_bytes == 4 {
3409                            // Bool-as-f32 masks (f32-uniform arena).
3410                            let c = sl(*cond, base, c_n.min(max_c).max(1));
3411                            for i in 0..len {
3412                                let ci = if *cond_scalar { 0 } else { i };
3413                                let ti = if *true_scalar { 0 } else { i };
3414                                let ei = if *false_scalar { 0 } else { i };
3415                                o[i] = if c[ci] != 0.0 { t[ti] } else { e[ei] };
3416                            }
3417                        } else {
3418                            let c = sl_i64(*cond, base, c_n.min(max_c).max(1));
3419                            for i in 0..len {
3420                                let ci = if *cond_scalar { 0 } else { i };
3421                                let ti = if *true_scalar { 0 } else { i };
3422                                let ei = if *false_scalar { 0 } else { i };
3423                                o[i] = if c[ci] != 0 { t[ti] } else { e[ei] };
3424                            }
3425                        }
3426                    } else if *cond_elem_bytes == 1 {
3427                        let c = &arena_buf[*cond..*cond + c_n.min(max_c).max(1)];
3428                        let t = sl(*on_true, base, t_n.min(max_t).max(1));
3429                        let e = sl(*on_false, base, f_n.min(max_f).max(1));
3430                        let o = sl_mut(*dst, base, len);
3431                        for i in 0..len {
3432                            let ci = if *cond_scalar { 0 } else { i };
3433                            let ti = if *true_scalar { 0 } else { i };
3434                            let ei = if *false_scalar { 0 } else { i };
3435                            o[i] = if c[ci] != 0 { t[ti] } else { e[ei] };
3436                        }
3437                    } else {
3438                        let c = sl(*cond, base, c_n.min(max_c).max(1));
3439                        let t = sl(*on_true, base, t_n.min(max_t).max(1));
3440                        let e = sl(*on_false, base, f_n.min(max_f).max(1));
3441                        let o = sl_mut(*dst, base, len);
3442                        for i in 0..len {
3443                            let ci = if *cond_scalar { 0 } else { i };
3444                            let ti = if *true_scalar { 0 } else { i };
3445                            let ei = if *false_scalar { 0 } else { i };
3446                            o[i] = if c[ci] != 0.0 { t[ti] } else { e[ei] };
3447                        }
3448                    }
3449                }
3450            }
3451
3452            Thunk::Fma {
3453                a,
3454                b,
3455                c,
3456                dst,
3457                len,
3458                elem_bytes,
3459            } => {
3460                let len = *len as usize;
3461                let eb = (*elem_bytes).max(1) as usize;
3462                let arena_len = arena_buf.len();
3463                let len = len
3464                    .min(arena_len.saturating_sub(*a) / eb)
3465                    .min(arena_len.saturating_sub(*b) / eb)
3466                    .min(arena_len.saturating_sub(*c) / eb)
3467                    .min(arena_len.saturating_sub(*dst) / eb);
3468                unsafe {
3469                    if *elem_bytes == 8 {
3470                        let av = sl_f64(*a, base, len);
3471                        let bv = sl_f64(*b, base, len);
3472                        let cv = sl_f64(*c, base, len);
3473                        let o = sl_mut_f64(*dst, base, len);
3474                        for i in 0..len {
3475                            o[i] = av[i].mul_add(bv[i], cv[i]);
3476                        }
3477                    } else {
3478                        let av = sl(*a, base, len);
3479                        let bv = sl(*b, base, len);
3480                        let cv = sl(*c, base, len);
3481                        let o = sl_mut(*dst, base, len);
3482                        for i in 0..len {
3483                            o[i] = av[i].mul_add(bv[i], cv[i]);
3484                        }
3485                    }
3486                }
3487            }
3488
3489            Thunk::ScatterAdd { .. } => exec_scatter_add(thunk, base),
3490            Thunk::ScatterNd { .. } => exec_scatter_nd(thunk, base),
3491            Thunk::ScatterElements { .. } => exec_scatter_elements(thunk, base),
3492            Thunk::GatherNd { .. } => exec_gather_nd(thunk, base),
3493            Thunk::GatherElements { .. } => exec_gather_elements(thunk, base),
3494            Thunk::GroupedMatMul {
3495                input,
3496                weight,
3497                expert_idx,
3498                dst,
3499                m,
3500                k_dim,
3501                n,
3502                num_experts,
3503                w_transposed,
3504            } => {
3505                let m = *m as usize;
3506                let k_dim = *k_dim as usize;
3507                let n = *n as usize;
3508                let num_experts = *num_experts as usize;
3509                let w_transposed = *w_transposed;
3510                unsafe {
3511                    let inp = sl(*input, base, m * k_dim);
3512                    let wt = sl(*weight, base, num_experts * k_dim * n);
3513                    let ids = sl(*expert_idx, base, m);
3514                    let out = sl_mut(*dst, base, m * n);
3515
3516                    // Counting-sort tokens by their assigned expert.
3517                    // counts[e] = how many tokens routed to expert e.
3518                    let mut counts = vec![0usize; num_experts];
3519                    for i in 0..m {
3520                        let e = ids[i] as usize;
3521                        debug_assert!(
3522                            e < num_experts,
3523                            "expert_idx out of range: {e} >= {num_experts}"
3524                        );
3525                        counts[e] += 1;
3526                    }
3527                    // Cumulative offsets into the packed buffer.
3528                    let mut offsets = vec![0usize; num_experts + 1];
3529                    for e in 0..num_experts {
3530                        offsets[e + 1] = offsets[e] + counts[e];
3531                    }
3532                    // Pack: each expert's rows land contiguously in `packed_in`.
3533                    // `original_pos[packed_idx] = original_token_idx` for the
3534                    // unpermute step at the end.
3535                    let mut packed_in = vec![0f32; m * k_dim];
3536                    let mut original_pos = vec![0usize; m];
3537                    let mut write_idx = vec![0usize; num_experts];
3538                    for i in 0..m {
3539                        let e = ids[i] as usize;
3540                        let dst_row = offsets[e] + write_idx[e];
3541                        packed_in[dst_row * k_dim..(dst_row + 1) * k_dim]
3542                            .copy_from_slice(&inp[i * k_dim..(i + 1) * k_dim]);
3543                        original_pos[dst_row] = i;
3544                        write_idx[e] += 1;
3545                    }
3546
3547                    // One BLAS sgemm per expert. Skip experts with no
3548                    // tokens — common at the tail when M is much smaller
3549                    // than num_experts × k.
3550                    let mut packed_out = vec![0f32; m * n];
3551                    let expert_stride = k_dim * n;
3552                    let gmm_ord = crate::moe_residency::next_gmm_ord();
3553                    let moe_layer = gmm_ord / 3;
3554                    for e in 0..num_experts {
3555                        let count = counts[e];
3556                        if count == 0 {
3557                            continue;
3558                        }
3559                        crate::moe_residency::record_expert_tokens(moe_layer, e, count);
3560                        let in_start = offsets[e];
3561                        let in_slice = &packed_in[in_start * k_dim..(in_start + count) * k_dim];
3562                        let w_slab: &[f32] =
3563                            if !crate::moe_residency::expert_on_device_for_layer(moe_layer, e) {
3564                                if let Some(ptr) =
3565                                    crate::moe_residency::host_expert_weight_ptr(gmm_ord, e)
3566                                {
3567                                    // Residency hands over `[K, N]` slabs by
3568                                    // construction. A folded transpose means the
3569                                    // bank is `[N, K]`, so the two do not compose
3570                                    // — and silently using one as the other reads
3571                                    // a correctly-sized block of the wrong values,
3572                                    // which no shape check would catch. Refuse.
3573                                    assert!(
3574                                        !w_transposed,
3575                                        "rlx-cpu: MoE host residency supplies [K, N] expert \
3576                                         slabs, but this GroupedMatMul folded a bank transpose \
3577                                         and reads [N, K]. The two are not composed; unbind \
3578                                         residency or disable the fold."
3579                                    );
3580                                    std::slice::from_raw_parts(ptr, expert_stride)
3581                                } else {
3582                                    &wt[e * expert_stride..(e + 1) * expert_stride]
3583                                }
3584                            } else {
3585                                &wt[e * expert_stride..(e + 1) * expert_stride]
3586                            };
3587                        let out_slice = &mut packed_out[in_start * n..(in_start + count) * n];
3588                        // Measured: routing this through `par_sgemm` REGRESSES decode
3589                        // (0.069 -> 0.085 s/token). At m == 1 the per-call work is a
3590                        // 1.5 MFLOP matvec, and decode issues 368 of these per token,
3591                        // so ~368 Rayon hand-offs cost more than the extra memory
3592                        // parallelism buys. Keep the serial call.
3593                        if w_transposed {
3594                            // Bank is `[E, N, K]`: B-transposed GEMM, no copy.
3595                            crate::blas::sgemm_bt(
3596                                in_slice, w_slab, out_slice, count, k_dim, n, 1.0,
3597                            );
3598                        } else {
3599                            crate::blas::sgemm(in_slice, w_slab, out_slice, count, k_dim, n);
3600                        }
3601                    }
3602
3603                    // Unpermute back to original token order.
3604                    for packed_idx in 0..m {
3605                        let i = original_pos[packed_idx];
3606                        out[i * n..(i + 1) * n]
3607                            .copy_from_slice(&packed_out[packed_idx * n..(packed_idx + 1) * n]);
3608                    }
3609                }
3610            }
3611
3612            Thunk::DequantGroupedMatMulGguf { .. } => {
3613                exec_dequant_grouped_mat_mul_gguf(thunk, base)
3614            }
3615            Thunk::DequantGroupedMatMulMlx { .. } => exec_dequant_grouped_mat_mul_mlx(thunk, base),
3616            Thunk::DequantMoEWeightsGguf { .. } => exec_dequant_mo_e_weights_gguf(thunk, base),
3617            Thunk::TopK {
3618                src,
3619                dst,
3620                outer,
3621                axis_dim,
3622                k,
3623                indices_i64,
3624            } => {
3625                let outer = *outer as usize;
3626                let axis_dim = *axis_dim as usize;
3627                let k = *k as usize;
3628                unsafe {
3629                    let inp = sl(*src, base, outer * axis_dim);
3630                    // Repeated argmax with masking. O(k * axis_dim) per row;
3631                    // good enough for small k (MoE typical k=2–8). For larger
3632                    // k a partial heap would win.
3633                    let mut row_buf: Vec<f32> = vec![0.0; axis_dim];
3634                    if *indices_i64 != 0 {
3635                        let out = sl_mut_i64(*dst, base, outer * k);
3636                        for o in 0..outer {
3637                            row_buf.copy_from_slice(&inp[o * axis_dim..(o + 1) * axis_dim]);
3638                            for ki in 0..k {
3639                                let mut best_i = 0usize;
3640                                let mut best_v = row_buf[0];
3641                                for i in 1..axis_dim {
3642                                    let v = row_buf[i];
3643                                    if v > best_v {
3644                                        best_v = v;
3645                                        best_i = i;
3646                                    }
3647                                }
3648                                out[o * k + ki] = best_i as i64;
3649                                row_buf[best_i] = f32::NEG_INFINITY;
3650                            }
3651                        }
3652                    } else {
3653                        let out = sl_mut(*dst, base, outer * k);
3654                        for o in 0..outer {
3655                            row_buf.copy_from_slice(&inp[o * axis_dim..(o + 1) * axis_dim]);
3656                            for ki in 0..k {
3657                                let mut best_i = 0usize;
3658                                let mut best_v = row_buf[0];
3659                                for i in 1..axis_dim {
3660                                    let v = row_buf[i];
3661                                    if v > best_v {
3662                                        best_v = v;
3663                                        best_i = i;
3664                                    }
3665                                }
3666                                out[o * k + ki] = best_i as f32;
3667                                row_buf[best_i] = f32::NEG_INFINITY;
3668                            }
3669                        }
3670                        if let Some(cap) = schedule.moe_topk_capture.as_ref() {
3671                            cap.push_topk_f32(&out[..outer * k], axis_dim);
3672                        }
3673                    }
3674                }
3675            }
3676
3677            Thunk::Reduce { .. } => exec_reduce(thunk, base),
3678            Thunk::Histogram { .. } => exec_histogram(thunk, base),
3679            Thunk::ArgReduce { .. } => exec_arg_reduce(thunk, base),
3680            Thunk::Conv2D1x1 { .. } => exec_conv2_d1x1(thunk, base),
3681            Thunk::Conv2D { .. } => exec_conv2_d(thunk, base),
3682            Thunk::Conv3d { .. } => exec_conv3d(thunk, base),
3683            Thunk::ConvTranspose3d { .. } => exec_conv_transpose3d(thunk, base),
3684            Thunk::Pool2D {
3685                src,
3686                dst,
3687                n,
3688                c,
3689                h,
3690                w,
3691                h_out,
3692                w_out,
3693                kh,
3694                kw,
3695                sh,
3696                sw,
3697                ph,
3698                pw,
3699                kind,
3700            } => {
3701                let n = *n as usize;
3702                let c = *c as usize;
3703                let h = *h as usize;
3704                let w = *w as usize;
3705                let h_out = *h_out as usize;
3706                let w_out = *w_out as usize;
3707                let kh = *kh as usize;
3708                let kw = *kw as usize;
3709                let sh = *sh as usize;
3710                let sw = *sw as usize;
3711                let ph = *ph as usize;
3712                let pw = *pw as usize;
3713                let kernel_area = (kh * kw) as f32;
3714                unsafe {
3715                    let inp = sl(*src, base, n * c * h * w);
3716                    let out = sl_mut(*dst, base, n * c * h_out * w_out);
3717                    // Each (n, c) plane is independent and writes a disjoint
3718                    // output region, so pooling fans out over the channel-batch
3719                    // when RLX_FAST_CONV is set.
3720                    let out_addr = out.as_mut_ptr() as usize;
3721                    let is_max = matches!(kind, ReduceOp::Max);
3722                    let is_mean = matches!(kind, ReduceOp::Mean);
3723                    // No-padding windows are *usually* fully in-bounds (the
3724                    // conv-net case), so the hot path drops the per-element
3725                    // bounds branches and hoists the reduce-op choice out of the
3726                    // loop. "Usually" is not "always": under ONNX `ceil_mode=1`
3727                    // the final window may start inside the input and overhang
3728                    // its end (259 frames, kernel/stride 100 -> 3 windows, the
3729                    // last spanning 200..300), and the fast path then indexes
3730                    // past the buffer. Require every window to fit before taking
3731                    // it; the general path below already treats the overhang as
3732                    // padding, which is what ONNX specifies.
3733                    let windows_fit = h_out.saturating_sub(1) * sh + kh <= h
3734                        && w_out.saturating_sub(1) * sw + kw <= w;
3735                    let nopad = ph == 0 && pw == 0 && windows_fit;
3736                    let pool_plane = |nc: usize| {
3737                        let ni = nc / c;
3738                        let ci = nc % c;
3739                        let in_chan = ni * c * h * w + ci * h * w;
3740                        let out_chan = ni * c * h_out * w_out + ci * h_out * w_out;
3741                        let op = out_addr as *mut f32;
3742                        for ho in 0..h_out {
3743                            for wo in 0..w_out {
3744                                let mut valid = 0.0f32;
3745                                let acc = if nopad {
3746                                    let row0 = in_chan + (ho * sh) * w + wo * sw;
3747                                    let mut a = if is_max { f32::NEG_INFINITY } else { 0.0 };
3748                                    for ki in 0..kh {
3749                                        let row = row0 + ki * w;
3750                                        if is_max {
3751                                            for kj in 0..kw {
3752                                                a = a.max(inp[row + kj]);
3753                                            }
3754                                        } else {
3755                                            for kj in 0..kw {
3756                                                a += inp[row + kj];
3757                                            }
3758                                        }
3759                                    }
3760                                    a
3761                                } else {
3762                                    let mut a = if is_max { f32::NEG_INFINITY } else { 0.0 };
3763                                    for ki in 0..kh {
3764                                        for kj in 0..kw {
3765                                            let hi = ho * sh + ki;
3766                                            let wi = wo * sw + kj;
3767                                            if hi < ph || wi < pw {
3768                                                continue;
3769                                            }
3770                                            let hi = hi - ph;
3771                                            let wi = wi - pw;
3772                                            if hi >= h || wi >= w {
3773                                                continue;
3774                                            }
3775                                            let v = inp[in_chan + hi * w + wi];
3776                                            if is_max {
3777                                                a = a.max(v);
3778                                            } else {
3779                                                a += v;
3780                                                valid += 1.0;
3781                                            }
3782                                        }
3783                                    }
3784                                    a
3785                                };
3786                                // Average over the elements that actually exist,
3787                                // not over the nominal window. ONNX's default is
3788                                // `count_include_pad = 0`, and a `ceil_mode`
3789                                // window that overhangs the input is not padding
3790                                // at all — those positions simply are not there.
3791                                // ChatterBox's speaker encoder pools 259 frames
3792                                // with kernel/stride 100: the third window holds
3793                                // 59 real frames, and dividing it by 100 scaled
3794                                // it by 0.59 against the reference.
3795                                let denom = if nopad { kernel_area } else { valid.max(1.0) };
3796                                let acc = if is_mean { acc / denom } else { acc };
3797                                *op.add(out_chan + ho * w_out + wo) = acc;
3798                            }
3799                        }
3800                    };
3801                    if fast_conv_enabled() && crate::pool::should_parallelize(n * c * h_out * w_out)
3802                    {
3803                        crate::pool::par_for(
3804                            n * c,
3805                            crate::pool::outer_chunk(n * c),
3806                            &|off, cnt| {
3807                                for nc in off..off + cnt {
3808                                    pool_plane(nc);
3809                                }
3810                            },
3811                        );
3812                    } else {
3813                        for nc in 0..n * c {
3814                            pool_plane(nc);
3815                        }
3816                    }
3817                }
3818            }
3819
3820            Thunk::Pool3D {
3821                src,
3822                dst,
3823                n,
3824                c,
3825                d,
3826                h,
3827                w,
3828                d_out,
3829                h_out,
3830                w_out,
3831                kd,
3832                kh,
3833                kw,
3834                sd,
3835                sh,
3836                sw,
3837                pd,
3838                ph,
3839                pw,
3840                kind,
3841            } => {
3842                let n = *n as usize;
3843                let c = *c as usize;
3844                let d = *d as usize;
3845                let h = *h as usize;
3846                let w = *w as usize;
3847                let d_out = *d_out as usize;
3848                let h_out = *h_out as usize;
3849                let w_out = *w_out as usize;
3850                let kd = *kd as usize;
3851                let kh = *kh as usize;
3852                let kw = *kw as usize;
3853                let sd = *sd as usize;
3854                let sh = *sh as usize;
3855                let sw = *sw as usize;
3856                let pd = *pd as usize;
3857                let ph = *ph as usize;
3858                let pw = *pw as usize;
3859                let kernel_vol = (kd * kh * kw) as f32;
3860                let is_max = matches!(kind, ReduceOp::Max);
3861                let is_mean = matches!(kind, ReduceOp::Mean);
3862                unsafe {
3863                    let inp = sl(*src, base, n * c * d * h * w);
3864                    let out = sl_mut(*dst, base, n * c * d_out * h_out * w_out);
3865                    for ni in 0..n {
3866                        for ci in 0..c {
3867                            let in_base = (ni * c + ci) * d * h * w;
3868                            let out_base = (ni * c + ci) * d_out * h_out * w_out;
3869                            for od in 0..d_out {
3870                                for ho in 0..h_out {
3871                                    for wo in 0..w_out {
3872                                        let mut acc = if is_max { f32::NEG_INFINITY } else { 0.0 };
3873                                        for kz in 0..kd {
3874                                            for ki in 0..kh {
3875                                                for kj in 0..kw {
3876                                                    let di = od * sd + kz;
3877                                                    let hi = ho * sh + ki;
3878                                                    let wi = wo * sw + kj;
3879                                                    if di < pd || hi < ph || wi < pw {
3880                                                        continue;
3881                                                    }
3882                                                    let di = di - pd;
3883                                                    let hi = hi - ph;
3884                                                    let wi = wi - pw;
3885                                                    if di >= d || hi >= h || wi >= w {
3886                                                        continue;
3887                                                    }
3888                                                    let v = inp[in_base + (di * h + hi) * w + wi];
3889                                                    if is_max {
3890                                                        acc = acc.max(v);
3891                                                    } else {
3892                                                        acc += v;
3893                                                    }
3894                                                }
3895                                            }
3896                                        }
3897                                        if is_mean {
3898                                            acc /= kernel_vol;
3899                                        }
3900                                        out[out_base + (od * h_out + ho) * w_out + wo] = acc;
3901                                    }
3902                                }
3903                            }
3904                        }
3905                    }
3906                }
3907            }
3908
3909            Thunk::ReluBackward { .. } => exec_relu_backward(thunk, base),
3910            Thunk::ReluBackwardF64 { .. } => exec_relu_backward_f64(thunk, base),
3911            Thunk::QMatMul { .. } => exec_q_mat_mul(thunk, base),
3912            Thunk::QConv2d {
3913                x,
3914                w,
3915                bias,
3916                out,
3917                n,
3918                c_in,
3919                h,
3920                w_in,
3921                c_out,
3922                h_out,
3923                w_out,
3924                kh,
3925                kw,
3926                sh,
3927                sw,
3928                ph,
3929                pw,
3930                dh,
3931                dw,
3932                groups,
3933                x_zp,
3934                w_zp,
3935                out_zp,
3936                mult,
3937            } => {
3938                let n = *n as usize;
3939                let c_in = *c_in as usize;
3940                let h = *h as usize;
3941                let w_in = *w_in as usize;
3942                let c_out = *c_out as usize;
3943                let h_out = *h_out as usize;
3944                let w_out = *w_out as usize;
3945                let kh = *kh as usize;
3946                let kw = *kw as usize;
3947                let sh = *sh as usize;
3948                let sw = *sw as usize;
3949                let ph = *ph as usize;
3950                let pw = *pw as usize;
3951                let dh = *dh as usize;
3952                let dw = *dw as usize;
3953                let groups = *groups as usize;
3954                let c_in_per_g = c_in / groups;
3955                let c_out_per_g = c_out / groups;
3956                unsafe {
3957                    let x_ptr = base.add(*x) as *const i8;
3958                    let w_ptr = base.add(*w) as *const i8;
3959                    let bias_ptr = base.add(*bias) as *const i32;
3960                    let out_ptr = base.add(*out) as *mut i8;
3961                    for ni in 0..n {
3962                        for co in 0..c_out {
3963                            let g = co / c_out_per_g;
3964                            let ci_start = g * c_in_per_g;
3965                            for ho in 0..h_out {
3966                                for wo in 0..w_out {
3967                                    let mut acc: i32 = *bias_ptr.add(co);
3968                                    for ci_off in 0..c_in_per_g {
3969                                        let ci = ci_start + ci_off;
3970                                        let in_chan = ((ni * c_in) + ci) * h * w_in;
3971                                        let wt_chan = ((co * c_in_per_g) + ci_off) * kh * kw;
3972                                        for ki in 0..kh {
3973                                            for kj in 0..kw {
3974                                                let hi = ho * sh + ki * dh;
3975                                                let wi = wo * sw + kj * dw;
3976                                                if hi < ph || wi < pw {
3977                                                    continue;
3978                                                }
3979                                                let hi = hi - ph;
3980                                                let wi = wi - pw;
3981                                                if hi >= h || wi >= w_in {
3982                                                    continue;
3983                                                }
3984                                                let xv = *x_ptr.add(in_chan + hi * w_in + wi)
3985                                                    as i32
3986                                                    - *x_zp;
3987                                                let wv = *w_ptr.add(wt_chan + ki * kw + kj) as i32
3988                                                    - *w_zp;
3989                                                acc += xv * wv;
3990                                            }
3991                                        }
3992                                    }
3993                                    let r = (acc as f32 * *mult).round() as i32 + *out_zp;
3994                                    let r = r.clamp(-128, 127) as i8;
3995                                    let dst = ((ni * c_out) + co) * h_out * w_out + ho * w_out + wo;
3996                                    *out_ptr.add(dst) = r;
3997                                }
3998                            }
3999                        }
4000                    }
4001                }
4002            }
4003
4004            Thunk::Quantize { .. } => exec_quantize(thunk, base),
4005            Thunk::Dequantize { .. } => exec_dequantize(thunk, base),
4006            Thunk::FakeQuantize { .. } => exec_fake_quantize(thunk, base),
4007            Thunk::ActivationBackward { .. } => exec_activation_backward(thunk, base),
4008            Thunk::ActivationBackwardF64 { .. } => exec_activation_backward_f64(thunk, base),
4009            Thunk::FakeQuantizeLSQ { .. } => exec_fake_quantize_l_s_q(thunk, base),
4010            Thunk::FakeQuantizeLSQBackwardX { .. } => {
4011                exec_fake_quantize_l_s_q_backward_x(thunk, base)
4012            }
4013            Thunk::FakeQuantizeLSQBackwardScale { .. } => {
4014                exec_fake_quantize_l_s_q_backward_scale(thunk, base)
4015            }
4016            Thunk::FakeQuantizeBackward { .. } => exec_fake_quantize_backward(thunk, base),
4017            Thunk::LayerNormBackwardInput { .. } => exec_layer_norm_backward_input(thunk, base),
4018            Thunk::BatchNormInferenceBackwardInput { .. } => {
4019                exec_batch_norm_inference_backward_input(thunk, base)
4020            }
4021            Thunk::BatchNormInferenceBackwardGamma { .. } => {
4022                exec_batch_norm_inference_backward_gamma(thunk, base)
4023            }
4024            Thunk::BatchNormInferenceBackwardBeta { .. } => {
4025                exec_batch_norm_inference_backward_beta(thunk, base)
4026            }
4027            Thunk::LayerNormBackwardGamma { .. } => exec_layer_norm_backward_gamma(thunk, base),
4028            Thunk::RmsNormBackwardInput { .. } => exec_rms_norm_backward_input(thunk, base),
4029            Thunk::RmsNormBackwardGamma { .. } => exec_rms_norm_backward_gamma(thunk, base),
4030            Thunk::RmsNormBackwardBeta { .. } => exec_rms_norm_backward_beta(thunk, base),
4031            Thunk::RopeBackward { .. } => exec_rope_backward(thunk, base),
4032            Thunk::CumsumBackward { .. } => exec_cumsum_backward(thunk, base),
4033            Thunk::GroupNormBackwardInput { .. } => exec_group_norm_backward_input(thunk, base),
4034            Thunk::GroupNormBackwardGamma { .. } => exec_group_norm_backward_gamma(thunk, base),
4035            Thunk::GroupNormBackwardBeta { .. } => exec_group_norm_backward_beta(thunk, base),
4036            Thunk::GatherBackward { .. } => exec_gather_backward(thunk, base),
4037            Thunk::MaxPool2dBackward { .. } => exec_max_pool2d_backward(thunk, base),
4038            Thunk::Conv2dBackwardInput { .. } => exec_conv2d_backward_input(thunk, base),
4039            Thunk::Conv2dBackwardWeight { .. } => exec_conv2d_backward_weight(thunk, base),
4040            Thunk::MaxPool3dBackward { .. } => exec_max_pool3d_backward(thunk, base),
4041            Thunk::Conv3dBackwardInput { .. } => exec_conv3d_backward_input(thunk, base),
4042            Thunk::Conv3dBackwardWeight { .. } => exec_conv3d_backward_weight(thunk, base),
4043            Thunk::Im2Col { .. } => exec_im2_col(thunk, base),
4044            Thunk::SoftmaxCrossEntropyDense { .. } => exec_softmax_cross_entropy_dense(thunk, base),
4045            Thunk::SoftmaxCrossEntropy { .. } => exec_softmax_cross_entropy(thunk, base),
4046            Thunk::SoftmaxCrossEntropyBackward { .. } => {
4047                exec_softmax_cross_entropy_backward(thunk, base)
4048            }
4049            Thunk::GatherAxis { .. } => exec_gather_axis(thunk, base),
4050            Thunk::Transpose {
4051                src,
4052                dst,
4053                in_total,
4054                out_dims,
4055                in_strides,
4056                elem_bytes,
4057            } => {
4058                // N-D index walk: for each output flat index, decompose into
4059                // multi-dim coords using out_dims, then dot with in_strides
4060                // to find the source flat index. Stride 0 = broadcast (read
4061                // the same input element repeatedly along that dim).
4062                let rank = out_dims.len();
4063                let total: usize = out_dims.iter().map(|&d| d as usize).product();
4064                // Empty output (e.g. Zipformer downsample pad `Expand` → `[0,1,C]`
4065                // when T is already a multiple of the stride) — nothing to write.
4066                if total == 0 {
4067                    // fall through
4068                } else {
4069                    let in_total = *in_total as usize;
4070                    unsafe {
4071                        if *elem_bytes == 1 {
4072                            // 1-byte dtypes (Bool / I8 / U8). Without this branch the
4073                            // `else` path below reads/writes 4 bytes per element via the
4074                            // f32 slice, corrupting e.g. a broadcast of the VITS attention
4075                            // mask (Bool, expanded over heads) — masking wrong positions.
4076                            let inp = arena_buf[*src..*src + in_total].to_vec();
4077                            let out = &mut arena_buf[*dst..*dst + total];
4078                            let mut idx = vec![0usize; rank];
4079                            for o in 0..total {
4080                                let mut src_idx = 0usize;
4081                                for d in 0..rank {
4082                                    src_idx += idx[d] * in_strides[d] as usize;
4083                                }
4084                                out[o] = inp[broadcast_src_index(src_idx, in_total)];
4085                                for d in (0..rank).rev() {
4086                                    idx[d] += 1;
4087                                    if idx[d] < out_dims[d] as usize {
4088                                        break;
4089                                    }
4090                                    idx[d] = 0;
4091                                }
4092                            }
4093                        } else if *elem_bytes == 8 {
4094                            let inp = sl_i64(*src, base, in_total);
4095                            let out = sl_mut_i64(*dst, base, total);
4096                            let mut idx = vec![0usize; rank];
4097                            for o in 0..total {
4098                                let mut src_idx = 0usize;
4099                                for d in 0..rank {
4100                                    src_idx += idx[d] * in_strides[d] as usize;
4101                                }
4102                                out[o] = inp[broadcast_src_index(src_idx, in_total)];
4103                                for d in (0..rank).rev() {
4104                                    idx[d] += 1;
4105                                    if idx[d] < out_dims[d] as usize {
4106                                        break;
4107                                    }
4108                                    idx[d] = 0;
4109                                }
4110                            }
4111                        } else if *elem_bytes == 16 {
4112                            // 16-byte dtypes (C128 = 2×f64 native / 4×f32 lanes).
4113                            // Copy each element as two i64 words; without this
4114                            // branch the `else` f32 path below reads 4 bytes per
4115                            // element and shatters the complex element (keeps only
4116                            // the first f32, drops the other 12 bytes).
4117                            let inp = sl_i64(*src, base, in_total * 2);
4118                            let out = sl_mut_i64(*dst, base, total * 2);
4119                            let mut idx = vec![0usize; rank];
4120                            for o in 0..total {
4121                                let mut src_idx = 0usize;
4122                                for d in 0..rank {
4123                                    src_idx += idx[d] * in_strides[d] as usize;
4124                                }
4125                                let s = broadcast_src_index(src_idx, in_total);
4126                                out[o * 2] = inp[s * 2];
4127                                out[o * 2 + 1] = inp[s * 2 + 1];
4128                                for d in (0..rank).rev() {
4129                                    idx[d] += 1;
4130                                    if idx[d] < out_dims[d] as usize {
4131                                        break;
4132                                    }
4133                                    idx[d] = 0;
4134                                }
4135                            }
4136                        } else {
4137                            let inp = sl(*src, base, in_total);
4138                            let out = sl_mut(*dst, base, total);
4139                            if rank == 4
4140                                && in_strides[0] == 0
4141                                && in_strides[2] == 0
4142                                && in_strides[3] == 0
4143                                && in_strides[1] != 0
4144                            {
4145                                // Per-channel broadcast out[n,c,h,w] = in[c*sc] — how
4146                                // a conv bias `[C]` reaches `[N,C,H,W]` (and its
4147                                // recompute in the backward graph). Fill each (n,c)
4148                                // plane with its scalar instead of an N-D index walk
4149                                // over every element; parallel over the channel-batch.
4150                                let d1 = out_dims[1] as usize;
4151                                let sc = in_strides[1] as usize;
4152                                let plane = (out_dims[2] as usize) * (out_dims[3] as usize);
4153                                let nc_total = (out_dims[0] as usize) * d1;
4154                                let out_addr = out.as_mut_ptr() as usize;
4155                                let fill = |nc0: usize, nc1: usize| {
4156                                    let op = out_addr as *mut f32;
4157                                    for nc in nc0..nc1 {
4158                                        let v = inp[(nc % d1) * sc];
4159                                        let base_off = nc * plane;
4160                                        for k in 0..plane {
4161                                            *op.add(base_off + k) = v;
4162                                        }
4163                                    }
4164                                };
4165                                if fast_conv_enabled() && crate::pool::should_parallelize(total) {
4166                                    crate::pool::par_for(
4167                                        nc_total,
4168                                        crate::pool::outer_chunk(nc_total),
4169                                        &|off, cnt| fill(off, off + cnt),
4170                                    );
4171                                } else {
4172                                    fill(0, nc_total);
4173                                }
4174                            } else if rank == 2 && in_strides[0] != 0 && in_strides[1] != 0 {
4175                                // Fast 2D transpose (the common matmul-backward case:
4176                                // xᵀ, wᵀ). out[i,j] = in[i*s0 + j*s1]; tiled over
4177                                // columns for write-locality, parallel over rows. Far
4178                                // cheaper than the general per-element index walk.
4179                                let d0 = out_dims[0] as usize;
4180                                let d1 = out_dims[1] as usize;
4181                                let s0 = in_strides[0] as usize;
4182                                let s1 = in_strides[1] as usize;
4183                                let out_addr = out.as_mut_ptr() as usize;
4184                                let tile = |i0: usize, i1: usize| {
4185                                    let op = out_addr as *mut f32;
4186                                    const T: usize = 32;
4187                                    let mut j0 = 0;
4188                                    while j0 < d1 {
4189                                        let j1 = (j0 + T).min(d1);
4190                                        for i in i0..i1 {
4191                                            let inb = i * s0;
4192                                            let outb = i * d1;
4193                                            for j in j0..j1 {
4194                                                *op.add(outb + j) = inp[inb + j * s1];
4195                                            }
4196                                        }
4197                                        j0 = j1;
4198                                    }
4199                                };
4200                                if fast_conv_enabled() && crate::pool::should_parallelize(total) {
4201                                    crate::pool::par_for(
4202                                        d0,
4203                                        crate::pool::outer_chunk(d0),
4204                                        &|off, cnt| tile(off, off + cnt),
4205                                    );
4206                                } else {
4207                                    tile(0, d0);
4208                                }
4209                            } else if rank >= 3
4210                                && in_strides[rank - 2] == 1
4211                                && in_strides[rank - 1] as usize == out_dims[rank - 2] as usize
4212                                && {
4213                                    // Leading axes untouched and contiguous, so
4214                                    // each batch plane is one dense 2-D block.
4215                                    let plane = (out_dims[rank - 2] as usize)
4216                                        * (out_dims[rank - 1] as usize);
4217                                    let mut expect = plane;
4218                                    let mut ok = true;
4219                                    for d in (0..rank - 2).rev() {
4220                                        if in_strides[d] as usize != expect {
4221                                            ok = false;
4222                                            break;
4223                                        }
4224                                        expect *= out_dims[d] as usize;
4225                                    }
4226                                    ok
4227                                }
4228                            {
4229                                // BATCHED 2-D transpose: the permutation swaps
4230                                // only the last two axes.
4231                                //
4232                                // Neither branch above catches this. The
4233                                // innermost stride is not 1 (it is the input's
4234                                // inner extent), and the rank is not 2, so it
4235                                // used to fall through to the general
4236                                // per-element index walk — measured at
4237                                // **103 MB/s** on an M4 Pro, against ~20 GB/s of
4238                                // memory bandwidth. A `[8, 2048, 1024]` f32
4239                                // bank took 653 ms.
4240                                //
4241                                // It is not a rare shape: `GroupedMatMul` wants
4242                                // `[E, in, out]` while GGUF stores expert banks
4243                                // as `[E, out, in]`, so every dense MoE layer
4244                                // transposes all three of its banks, and
4245                                // attention permutes `[B, H, S, D]` the same
4246                                // way. Reuse the tiling the rank-2 branch uses,
4247                                // once per plane.
4248                                let d_i = out_dims[rank - 2] as usize;
4249                                let d_o = out_dims[rank - 1] as usize;
4250                                let plane = d_i * d_o;
4251                                let batches = total / plane.max(1);
4252                                let out_addr = out.as_mut_ptr() as usize;
4253                                let in_addr = inp.as_ptr() as usize;
4254                                let plane_tp = |b0: usize, b1: usize| {
4255                                    const T: usize = 32;
4256                                    for b in b0..b1 {
4257                                        let ip = (in_addr as *const f32).add(b * plane);
4258                                        let op = (out_addr as *mut f32).add(b * plane);
4259                                        let mut i0 = 0;
4260                                        while i0 < d_i {
4261                                            let i1 = (i0 + T).min(d_i);
4262                                            let mut o0 = 0;
4263                                            while o0 < d_o {
4264                                                let o1 = (o0 + T).min(d_o);
4265                                                for i in i0..i1 {
4266                                                    for o in o0..o1 {
4267                                                        *op.add(i * d_o + o) = *ip.add(o * d_i + i);
4268                                                    }
4269                                                }
4270                                                o0 = o1;
4271                                            }
4272                                            i0 = i1;
4273                                        }
4274                                    }
4275                                };
4276                                if crate::pool::should_parallelize(total) && batches >= 2 {
4277                                    crate::pool::par_for(
4278                                        batches,
4279                                        crate::pool::outer_chunk(batches),
4280                                        &|off, cnt| plane_tp(off, off + cnt),
4281                                    );
4282                                } else {
4283                                    plane_tp(0, batches);
4284                                }
4285                            } else if rank >= 3
4286                                && *in_strides.last().unwrap_or(&0) == 1
4287                                && out_dims[rank - 1] as usize >= 8
4288                            {
4289                                // Innermost dim contiguous in both layouts: copy one
4290                                // row at a time (memcpy) instead of per-element index
4291                                // walk. Covers attention BSHD↔BHSD and most Expand/
4292                                // permute patterns in DiT.
4293                                let row = out_dims[rank - 1] as usize;
4294                                let planes = total / row;
4295                                let s_last = in_strides[rank - 1] as usize; // 1
4296                                let _ = s_last;
4297                                let out_addr = out.as_mut_ptr() as usize;
4298                                let in_addr = inp.as_ptr() as usize;
4299                                let dims = out_dims.to_vec();
4300                                let strides = in_strides.to_vec();
4301                                let copy_planes = |p0: usize, p1: usize| {
4302                                    let mut idx = vec![0usize; rank];
4303                                    let mut rem = p0;
4304                                    // Decode plane index into leading coords (exclude last dim).
4305                                    for d in (0..rank - 1).rev() {
4306                                        let dim = dims[d] as usize;
4307                                        idx[d] = rem % dim;
4308                                        rem /= dim;
4309                                    }
4310                                    for p in p0..p1 {
4311                                        let mut src = 0usize;
4312                                        for d in 0..rank - 1 {
4313                                            src += idx[d] * strides[d] as usize;
4314                                        }
4315                                        std::ptr::copy_nonoverlapping(
4316                                            (in_addr as *const f32).add(src),
4317                                            (out_addr as *mut f32).add(p * row),
4318                                            row,
4319                                        );
4320                                        for d in (0..rank - 1).rev() {
4321                                            idx[d] += 1;
4322                                            if idx[d] < dims[d] as usize {
4323                                                break;
4324                                            }
4325                                            idx[d] = 0;
4326                                        }
4327                                    }
4328                                };
4329                                if crate::pool::should_parallelize(total) && planes >= 4 {
4330                                    crate::pool::par_for(
4331                                        planes,
4332                                        crate::pool::outer_chunk(planes),
4333                                        &|off, cnt| copy_planes(off, off + cnt),
4334                                    );
4335                                } else {
4336                                    copy_planes(0, planes);
4337                                }
4338                            } else if fast_conv_enabled() && crate::pool::should_parallelize(total)
4339                            {
4340                                // Parallel: each chunk seeds its starting multi-index
4341                                // from `off`, then walks incrementally. Output writes
4342                                // are disjoint per `o`.
4343                                let out_addr = out.as_mut_ptr() as usize;
4344                                crate::pool::par_for(
4345                                    total,
4346                                    crate::pool::chunk_floor(total),
4347                                    &|off, cnt| {
4348                                        let mut idx = vec![0usize; rank];
4349                                        let mut rem = off;
4350                                        for d in (0..rank).rev() {
4351                                            let dim = out_dims[d] as usize;
4352                                            idx[d] = rem % dim;
4353                                            rem /= dim;
4354                                        }
4355                                        for o in off..off + cnt {
4356                                            let mut src_idx = 0usize;
4357                                            for d in 0..rank {
4358                                                src_idx += idx[d] * in_strides[d] as usize;
4359                                            }
4360                                            let v = inp[broadcast_src_index(src_idx, in_total)];
4361                                            *((out_addr as *mut f32).add(o)) = v;
4362                                            for d in (0..rank).rev() {
4363                                                idx[d] += 1;
4364                                                if idx[d] < out_dims[d] as usize {
4365                                                    break;
4366                                                }
4367                                                idx[d] = 0;
4368                                            }
4369                                        }
4370                                    },
4371                                );
4372                            } else {
4373                                let mut idx = vec![0usize; rank];
4374                                for o in 0..total {
4375                                    let mut src_idx = 0usize;
4376                                    for d in 0..rank {
4377                                        src_idx += idx[d] * in_strides[d] as usize;
4378                                    }
4379                                    out[o] = inp[broadcast_src_index(src_idx, in_total)];
4380                                    for d in (0..rank).rev() {
4381                                        idx[d] += 1;
4382                                        if idx[d] < out_dims[d] as usize {
4383                                            break;
4384                                        }
4385                                        idx[d] = 0;
4386                                    }
4387                                }
4388                            }
4389                        }
4390                    }
4391                } // total != 0
4392            }
4393
4394            Thunk::CustomOp { .. } => exec_custom_op(thunk, base, arena_buf.len()),
4395            Thunk::Reverse { .. } => exec_reverse(thunk, base),
4396        }
4397        if trace_done {
4398            eprintln!("[thunk {i} done]");
4399        }
4400        if write_trace {
4401            let lo = wt_before
4402                .iter()
4403                .zip(arena_buf.iter())
4404                .position(|(a, b)| a != b);
4405            match lo {
4406                None => eprintln!("[wt {i}] {} wrote nothing", thunk_kind_name(thunk)),
4407                Some(lo) => {
4408                    let hi = wt_before.len()
4409                        - wt_before
4410                            .iter()
4411                            .zip(arena_buf.iter())
4412                            .rev()
4413                            .position(|(a, b)| a != b)
4414                            .unwrap();
4415                    // Hash the CONTENT, not the offsets: under
4416                    // `RLX_ARENA_NO_REUSE` every slot moves, so only a
4417                    // content checksum lines up thunk-for-thunk between the
4418                    // two layouts and pins down the first op that diverges.
4419                    let h = arena_buf[lo..hi]
4420                        .iter()
4421                        .fold(0xcbf2_9ce4_8422_2325u64, |a, b| {
4422                            (a ^ *b as u64).wrapping_mul(0x1000_0000_01b3)
4423                        });
4424                    eprintln!(
4425                        "[wt {i}] {} wrote [{lo},{hi}) len={} hash={h:016x}",
4426                        thunk_kind_name(thunk),
4427                        hi - lo
4428                    );
4429                    wt_before.copy_from_slice(arena_buf);
4430                }
4431            }
4432        }
4433    }
4434    if profile {
4435        if let Some((pn, pt)) = prof_prev.take() {
4436            profile_record(pn, pt.elapsed());
4437        }
4438        // Auto-dump under RLX_PROFILE_THUNKS so any binary shows where the run's
4439        // time went (per execute_thunks call — the hot subgraph stands out).
4440        dump_thunk_profile();
4441    }
4442}
4443
4444/// `dots[j] = Σ_d q[d]·k[keys[j]·stride + off + d]`, summed in `d` order with
4445/// one multiply and one add per term — exactly the scalar loop
4446/// `dot += q[d] * k[d]`, so bit-identical to it. The scalar loop is a serial
4447/// dependency on one accumulator (an add latency per element); here eight keys
4448/// run in the eight lanes of one AVX register, fed by an 8×8 transpose of
4449/// their rows.
4450#[cfg(target_arch = "x86_64")]
4451pub(super) fn dots_by_key_lanes(
4452    q: &[f32],
4453    k: &[f32],
4454    keys: &[usize],
4455    stride: usize,
4456    off: usize,
4457    dots: &mut [f32],
4458) {
4459    let n = q.len();
4460    let avx = {
4461        static AVX: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4462        *AVX.get_or_init(|| std::is_x86_feature_detected!("avx"))
4463    };
4464    let lanes = if avx && n.is_multiple_of(8) {
4465        keys.len()
4466    } else {
4467        0
4468    };
4469    for (j, group) in keys[..lanes].chunks(8).enumerate() {
4470        // A short last group repeats its last key in the spare lanes.
4471        let rows: [&[f32]; 8] = std::array::from_fn(|l| {
4472            let ki = group[l.min(group.len() - 1)];
4473            &k[ki * stride + off..ki * stride + off + n]
4474        });
4475        // SAFETY: AVX was detected; every row holds `n` floats.
4476        let out = unsafe { dot8_avx(q, rows) };
4477        dots[8 * j..8 * j + group.len()].copy_from_slice(&out[..group.len()]);
4478    }
4479    for (o, &ki) in dots[lanes..].iter_mut().zip(&keys[lanes..]) {
4480        let row = &k[ki * stride + off..ki * stride + off + n];
4481        let mut dot = 0f32;
4482        for d in 0..n {
4483            dot += q[d] * row[d];
4484        }
4485        *o = dot;
4486    }
4487}
4488
4489/// Eight [`dots_by_key_lanes`] dots, one per lane. `q.len()` is a multiple
4490/// of 8.
4491#[cfg(target_arch = "x86_64")]
4492#[target_feature(enable = "avx")]
4493unsafe fn dot8_avx(q: &[f32], rows: [&[f32]; 8]) -> [f32; 8] {
4494    use std::arch::x86_64::*;
4495    let mut acc = _mm256_setzero_ps();
4496    for d0 in (0..q.len()).step_by(8) {
4497        // SAFETY: `d0 + 8 <= n` for `q` and every row. (Not `array::from_fn`:
4498        // a closure does not inherit `#[target_feature]`.)
4499        let mut r = [_mm256_setzero_ps(); 8];
4500        for (rl, row) in r.iter_mut().zip(rows) {
4501            *rl = unsafe { _mm256_loadu_ps(row[d0..].as_ptr()) };
4502        }
4503        // 8×8 transpose: `t[d]` holds element `d0 + d` of the eight rows.
4504        let a0 = _mm256_unpacklo_ps(r[0], r[1]);
4505        let a1 = _mm256_unpackhi_ps(r[0], r[1]);
4506        let a2 = _mm256_unpacklo_ps(r[2], r[3]);
4507        let a3 = _mm256_unpackhi_ps(r[2], r[3]);
4508        let a4 = _mm256_unpacklo_ps(r[4], r[5]);
4509        let a5 = _mm256_unpackhi_ps(r[4], r[5]);
4510        let a6 = _mm256_unpacklo_ps(r[6], r[7]);
4511        let a7 = _mm256_unpackhi_ps(r[6], r[7]);
4512        let b0 = _mm256_shuffle_ps::<0x44>(a0, a2);
4513        let b1 = _mm256_shuffle_ps::<0xEE>(a0, a2);
4514        let b2 = _mm256_shuffle_ps::<0x44>(a1, a3);
4515        let b3 = _mm256_shuffle_ps::<0xEE>(a1, a3);
4516        let b4 = _mm256_shuffle_ps::<0x44>(a4, a6);
4517        let b5 = _mm256_shuffle_ps::<0xEE>(a4, a6);
4518        let b6 = _mm256_shuffle_ps::<0x44>(a5, a7);
4519        let b7 = _mm256_shuffle_ps::<0xEE>(a5, a7);
4520        let t = [
4521            _mm256_permute2f128_ps::<0x20>(b0, b4),
4522            _mm256_permute2f128_ps::<0x20>(b1, b5),
4523            _mm256_permute2f128_ps::<0x20>(b2, b6),
4524            _mm256_permute2f128_ps::<0x20>(b3, b7),
4525            _mm256_permute2f128_ps::<0x31>(b0, b4),
4526            _mm256_permute2f128_ps::<0x31>(b1, b5),
4527            _mm256_permute2f128_ps::<0x31>(b2, b6),
4528            _mm256_permute2f128_ps::<0x31>(b3, b7),
4529        ];
4530        for (d, td) in t.into_iter().enumerate() {
4531            acc = _mm256_add_ps(acc, _mm256_mul_ps(_mm256_set1_ps(q[d0 + d]), td));
4532        }
4533    }
4534    let mut out = [0f32; 8];
4535    // SAFETY: `out` holds 8 floats.
4536    unsafe { _mm256_storeu_ps(out.as_mut_ptr(), acc) };
4537    out
4538}
4539
4540#[inline(always)]
4541pub(crate) fn exec_nop(t: &Thunk) {
4542    let Thunk::Nop = t else { unreachable!() };
4543    {}
4544}