Skip to main content

rlx_cpu/thunk/ops/
reduce.rs

1#![allow(unsafe_op_in_unsafe_fn)]
2use crate::thunk::*;
3
4/// Fused sampling step: logits → top-k filter → top-p truncation
5/// → softmax → multinomial sample. Operates on one row of length
6/// `vocab` and returns the sampled index. Plan #42.
7///
8/// Internal scratch is on the stack via SmallVec-style fallback —
9/// for `vocab > 8192` we heap-allocate a working buffer; below
10/// that we keep things in a fixed array. (TODO: thread the
11/// scratch through ThunkSchedule like sdpa_scores does.)
12pub(crate) fn sample_row(
13    logits: &[f32],
14    top_k: usize,
15    top_p: f32,
16    temperature: f32,
17    rng: &mut rlx_ir::Philox4x32,
18) -> usize {
19    let v = logits.len();
20    if v == 0 {
21        return 0;
22    }
23    let temp = temperature.max(1e-6);
24    // Copy + temperature-scale into a working buffer.
25    let mut scaled: Vec<f32> = logits.iter().map(|&x| x / temp).collect();
26
27    // Top-k: zero out everything but the k largest by setting to -inf.
28    if top_k > 0 && top_k < v {
29        // Partial selection: find k-th largest then mask below.
30        let mut indexed: Vec<(usize, f32)> = scaled.iter().copied().enumerate().collect();
31        // Sort descending; partial would be O(n log k), full sort is fine
32        // for typical vocab sizes (32k-128k) — single-row work.
33        indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
34        let cutoff = indexed[top_k - 1].1;
35        for x in scaled.iter_mut() {
36            if *x < cutoff {
37                *x = f32::NEG_INFINITY;
38            }
39        }
40    }
41
42    // Stable softmax.
43    let mut max_l = f32::NEG_INFINITY;
44    for &x in &scaled {
45        if x > max_l {
46            max_l = x;
47        }
48    }
49    let mut sum = 0.0f32;
50    for x in scaled.iter_mut() {
51        *x = (*x - max_l).exp();
52        sum += *x;
53    }
54    let inv = 1.0 / sum.max(f32::MIN_POSITIVE);
55    for x in scaled.iter_mut() {
56        *x *= inv;
57    }
58
59    // Top-p: keep the smallest set of tokens whose cumulative
60    // probability exceeds top_p (after sorting descending).
61    if top_p < 1.0 {
62        let mut indexed: Vec<(usize, f32)> = scaled.iter().copied().enumerate().collect();
63        indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
64        let mut cum = 0.0f32;
65        let mut keep = vec![false; v];
66        for (idx, p) in indexed.iter() {
67            keep[*idx] = true;
68            cum += *p;
69            if cum >= top_p {
70                break;
71            }
72        }
73        let mut new_sum = 0.0f32;
74        for (i, x) in scaled.iter_mut().enumerate() {
75            if !keep[i] {
76                *x = 0.0;
77            }
78            new_sum += *x;
79        }
80        let inv = 1.0 / new_sum.max(f32::MIN_POSITIVE);
81        for x in scaled.iter_mut() {
82            *x *= inv;
83        }
84    }
85
86    // Multinomial sample via inverse-CDF.
87    let r = rng.next_f32();
88    let mut acc = 0.0f32;
89    for (i, &p) in scaled.iter().enumerate() {
90        acc += p;
91        if r <= acc {
92            return i;
93        }
94    }
95    v - 1 // floating-point edge case fallback
96}
97
98#[allow(unused_variables)]
99pub(crate) fn compile_sample(
100    node: &rlx_ir::Node,
101    graph: &Graph,
102    arena: &crate::arena::Arena,
103    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
104    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
105    rng: rlx_ir::RngOptions,
106) -> Thunk {
107    let Op::Sample {
108        top_k,
109        top_p,
110        temperature,
111        seed,
112    } = &node.op
113    else {
114        unreachable!()
115    };
116    {
117        let in_shape = &graph.node(node.inputs[0]).shape;
118        // Logits are [batch, vocab] (or [vocab] → batch=1).
119        let (batch, vocab) = if in_shape.rank() >= 2 {
120            (
121                in_shape.dim(0).unwrap_static(),
122                in_shape.dim(in_shape.rank() - 1).unwrap_static(),
123            )
124        } else {
125            (1, in_shape.num_elements().unwrap_or(0))
126        };
127        Thunk::Sample {
128            logits: node_offset(arena, node.inputs[0]),
129            dst: node_offset(arena, node.id),
130            batch: batch as u32,
131            vocab: vocab as u32,
132            top_k: *top_k as u32,
133            top_p: *top_p,
134            temperature: *temperature,
135            seed: *seed,
136        }
137    }
138}
139
140#[allow(unused_variables)]
141pub(crate) fn compile_rng_normal(
142    node: &rlx_ir::Node,
143    graph: &Graph,
144    arena: &crate::arena::Arena,
145    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
146    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
147    rng: rlx_ir::RngOptions,
148) -> Thunk {
149    let Op::RngNormal {
150        mean,
151        scale,
152        key,
153        op_seed,
154    } = &node.op
155    else {
156        unreachable!()
157    };
158    Thunk::RngNormal {
159        dst: node_offset(arena, node.id),
160        len: node.shape.num_elements().unwrap_or(0) as u32,
161        mean: *mean,
162        scale: *scale,
163        key: *key,
164        op_seed: *op_seed,
165    }
166}
167
168#[allow(unused_variables)]
169pub(crate) fn compile_rng_uniform(
170    node: &rlx_ir::Node,
171    graph: &Graph,
172    arena: &crate::arena::Arena,
173    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
174    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
175    rng: rlx_ir::RngOptions,
176) -> Thunk {
177    let Op::RngUniform {
178        low,
179        high,
180        key,
181        op_seed,
182    } = &node.op
183    else {
184        unreachable!()
185    };
186    Thunk::RngUniform {
187        dst: node_offset(arena, node.id),
188        len: node.shape.num_elements().unwrap_or(0) as u32,
189        low: *low,
190        high: *high,
191        key: *key,
192        op_seed: *op_seed,
193    }
194}
195
196#[allow(unused_variables)]
197pub(crate) fn compile_cumsum(
198    node: &rlx_ir::Node,
199    graph: &Graph,
200    arena: &crate::arena::Arena,
201    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
202    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
203    rng: rlx_ir::RngOptions,
204) -> Thunk {
205    let Op::Cumsum { axis, exclusive } = &node.op else {
206        unreachable!()
207    };
208    {
209        // For now CPU only supports last-axis cumsum (the
210        // common case for sampling / ragged offsets).
211        // Other axes can lower via Transpose → Cumsum →
212        // Transpose; not on the hot path today.
213        let rank = node.shape.rank();
214        let ax = if *axis < 0 {
215            (rank as i32 + axis) as usize
216        } else {
217            *axis as usize
218        };
219        assert_eq!(
220            ax,
221            rank - 1,
222            "Cumsum only supports the last axis on CPU today"
223        );
224        let cols = node.shape.dim(ax).unwrap_static();
225        let total = node.shape.num_elements().unwrap();
226        Thunk::Cumsum {
227            src: node_offset(arena, node.inputs[0]),
228            dst: node_offset(arena, node.id),
229            rows: (total / cols) as u32,
230            cols: cols as u32,
231            exclusive: *exclusive,
232            dtype: node.shape.dtype(),
233        }
234    }
235}
236
237#[allow(unused_variables)]
238pub(crate) fn compile_top_k(
239    node: &rlx_ir::Node,
240    graph: &Graph,
241    arena: &crate::arena::Arena,
242    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
243    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
244    rng: rlx_ir::RngOptions,
245) -> Thunk {
246    let Op::TopK { k } = &node.op else {
247        unreachable!()
248    };
249    {
250        let in_shape = &graph.node(node.inputs[0]).shape;
251        let rank = in_shape.rank();
252        let axis_dim = in_shape.dim(rank - 1).unwrap_static();
253        let outer = in_shape.num_elements().unwrap() / axis_dim;
254        let indices_i64 = u8::from(graph.node(node.id).shape.dtype() == rlx_ir::DType::I64);
255        Thunk::TopK {
256            src: node_offset(arena, node.inputs[0]),
257            dst: node_offset(arena, node.id),
258            outer: outer as u32,
259            axis_dim: axis_dim as u32,
260            k: *k as u32,
261            indices_i64,
262        }
263    }
264}
265
266#[allow(unused_variables)]
267pub(crate) fn compile_reduce(
268    node: &rlx_ir::Node,
269    graph: &Graph,
270    arena: &crate::arena::Arena,
271    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
272    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
273    rng: rlx_ir::RngOptions,
274) -> Thunk {
275    let Op::Reduce {
276        op,
277        axes,
278        keep_dim: _,
279    } = &node.op
280    else {
281        unreachable!()
282    };
283    {
284        // Decompose the input shape into [outer, reduced, inner]
285        // around the reduced axis range. Non-contiguous reduced
286        // axes aren't supported here — caller must transpose them
287        // contiguous first (the coverage tool would surface the
288        // gap if a model needs it).
289        let in_shape = &graph.node(node.inputs[0]).shape;
290        let rank = in_shape.rank();
291        let mut sorted = axes.clone();
292        sorted.sort();
293        sorted.dedup();
294        let contiguous = sorted.windows(2).all(|w| w[1] == w[0] + 1)
295            && !sorted.is_empty()
296            && *sorted.last().unwrap() < rank;
297        if !contiguous {
298            Thunk::Nop
299        } else {
300            let first = sorted[0];
301            let last = *sorted.last().unwrap();
302            let outer: usize = (0..first)
303                .map(|i| in_shape.dim(i).unwrap_static())
304                .product::<usize>()
305                .max(1);
306            let reduced: usize = (first..=last)
307                .map(|i| in_shape.dim(i).unwrap_static())
308                .product();
309            let inner: usize = (last + 1..rank)
310                .map(|i| in_shape.dim(i).unwrap_static())
311                .product::<usize>()
312                .max(1);
313            let src = node_offset(arena, node.inputs[0]);
314            let dst = node_offset(arena, node.id);
315            if node.shape.dtype() == rlx_ir::DType::F64 && matches!(op, ReduceOp::Sum) {
316                Thunk::ReduceSumF64 {
317                    src,
318                    dst,
319                    outer: outer as u32,
320                    reduced: reduced as u32,
321                    inner: inner as u32,
322                }
323            } else {
324                Thunk::Reduce {
325                    src,
326                    dst,
327                    outer: outer as u32,
328                    reduced: reduced as u32,
329                    inner: inner as u32,
330                    op: *op,
331                }
332            }
333        }
334    }
335}
336
337#[allow(unused_variables)]
338pub(crate) fn compile_cumsum_backward(
339    node: &rlx_ir::Node,
340    graph: &Graph,
341    arena: &crate::arena::Arena,
342    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
343    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
344    rng: rlx_ir::RngOptions,
345) -> Thunk {
346    let Op::CumsumBackward { exclusive, .. } = &node.op else {
347        unreachable!()
348    };
349    {
350        let dy_shape = &graph.node(node.inputs[0]).shape;
351        let rank = dy_shape.rank();
352        let cols = dy_shape.dim(rank - 1).unwrap_static();
353        let rows = dy_shape.num_elements().unwrap() / cols;
354        Thunk::CumsumBackward {
355            dy: node_offset(arena, node.inputs[0]),
356            dx: node_offset(arena, node.id),
357            rows: rows as u32,
358            cols: cols as u32,
359            exclusive: *exclusive,
360        }
361    }
362}
363
364#[allow(unused_variables)]
365pub(crate) fn compile_softmax_cross_entropy(
366    node: &rlx_ir::Node,
367    graph: &Graph,
368    arena: &crate::arena::Arena,
369    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
370    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
371    rng: rlx_ir::RngOptions,
372) -> Thunk {
373    let Op::SoftmaxCrossEntropy = &node.op else {
374        unreachable!()
375    };
376    {
377        let logits_shape = &graph.node(node.inputs[0]).shape;
378        if logits_shape.rank() == 2 {
379            Thunk::SoftmaxCrossEntropyDense {
380                logits: node_offset(arena, node.inputs[0]),
381                targets: node_offset(arena, node.inputs[1]),
382                dst: node_offset(arena, node.id),
383                n: logits_shape.dim(0).unwrap_static() as u32,
384                c: logits_shape.dim(1).unwrap_static() as u32,
385            }
386        } else {
387            Thunk::Nop
388        }
389    }
390}
391
392#[allow(unused_variables)]
393pub(crate) fn compile_softmax_cross_entropy_with_logits(
394    node: &rlx_ir::Node,
395    graph: &Graph,
396    arena: &crate::arena::Arena,
397    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
398    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
399    rng: rlx_ir::RngOptions,
400) -> Thunk {
401    let Op::SoftmaxCrossEntropyWithLogits = &node.op else {
402        unreachable!()
403    };
404    {
405        let logits_shape = &graph.node(node.inputs[0]).shape;
406        if logits_shape.rank() == 2 {
407            Thunk::SoftmaxCrossEntropy {
408                logits: node_offset(arena, node.inputs[0]),
409                labels: node_offset(arena, node.inputs[1]),
410                dst: node_offset(arena, node.id),
411                n: logits_shape.dim(0).unwrap_static() as u32,
412                c: logits_shape.dim(1).unwrap_static() as u32,
413            }
414        } else {
415            Thunk::Nop
416        }
417    }
418}
419
420#[allow(unused_variables)]
421pub(crate) fn compile_softmax_cross_entropy_backward(
422    node: &rlx_ir::Node,
423    graph: &Graph,
424    arena: &crate::arena::Arena,
425    matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
426    rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
427    rng: rlx_ir::RngOptions,
428) -> Thunk {
429    let Op::SoftmaxCrossEntropyBackward = &node.op else {
430        unreachable!()
431    };
432    {
433        let logits_shape = &graph.node(node.inputs[0]).shape;
434        if logits_shape.rank() == 2 {
435            Thunk::SoftmaxCrossEntropyBackward {
436                logits: node_offset(arena, node.inputs[0]),
437                labels: node_offset(arena, node.inputs[1]),
438                d_loss: node_offset(arena, node.inputs[2]),
439                dlogits: node_offset(arena, node.id),
440                n: logits_shape.dim(0).unwrap_static() as u32,
441                c: logits_shape.dim(1).unwrap_static() as u32,
442            }
443        } else {
444            Thunk::Nop
445        }
446    }
447}
448
449#[inline(always)]
450pub(crate) fn exec_reduce_sum_f64(t: &Thunk, base: *mut u8) {
451    let Thunk::ReduceSumF64 {
452        src,
453        dst,
454        outer,
455        reduced,
456        inner,
457    } = t
458    else {
459        unreachable!()
460    };
461    {
462        let (o, r, n) = (*outer as usize, *reduced as usize, *inner as usize);
463        unsafe {
464            let inp = sl_f64(*src, base, o * r * n);
465            let out = sl_mut_f64(*dst, base, o * n);
466            reduce_sum_f64(inp, out, o, r, n);
467        }
468    }
469}
470
471#[inline(always)]
472pub(crate) fn exec_softmax(t: &Thunk, base: *mut u8) {
473    let Thunk::Softmax { data, rows, cols } = t else {
474        unreachable!()
475    };
476    {
477        let (rows, cols) = (*rows as usize, *cols as usize);
478        unsafe {
479            crate::kernels::neon_softmax(sl_mut(*data, base, rows * cols), rows, cols);
480        }
481    }
482}
483
484#[inline(always)]
485pub(crate) fn exec_cumsum(t: &Thunk, base: *mut u8) {
486    let Thunk::Cumsum {
487        src,
488        dst,
489        rows,
490        cols,
491        exclusive,
492        dtype,
493    } = t
494    else {
495        unreachable!()
496    };
497    unsafe {
498        cumsum_typed(
499            base,
500            *src,
501            *dst,
502            *rows as usize,
503            *cols as usize,
504            *exclusive,
505            *dtype,
506        );
507    }
508}
509
510/// Row-wise cumulative sum along the last axis, honoring the element dtype. f32 is
511/// the hot path (sampling / ragged offsets); I64 covers integer position ids /
512/// mask cumsums (the f32 path would reinterpret the 8-byte i64 buffer as f32 and
513/// scramble it, e.g. MOSS-TTS's RoPE `cumsum(attention_mask)`).
514pub(crate) unsafe fn cumsum_typed(
515    base: *mut u8,
516    src: usize,
517    dst: usize,
518    rows: usize,
519    cols: usize,
520    exclusive: bool,
521    dtype: rlx_ir::DType,
522) {
523    macro_rules! scan {
524        ($sl:expr, $slm:expr, $zero:expr) => {{
525            let s = $sl(src, base, rows * cols);
526            let d = $slm(dst, base, rows * cols);
527            for r in 0..rows {
528                let mut acc = $zero;
529                for c in 0..cols {
530                    if exclusive {
531                        d[r * cols + c] = acc;
532                        acc += s[r * cols + c];
533                    } else {
534                        acc += s[r * cols + c];
535                        d[r * cols + c] = acc;
536                    }
537                }
538            }
539        }};
540    }
541    match dtype {
542        rlx_ir::DType::I64 => scan!(sl_i64, sl_mut_i64, 0i64),
543        rlx_ir::DType::I32 => scan!(sl_i32, sl_mut_i32, 0i32),
544        _ => scan!(sl, sl_mut, 0.0f32),
545    }
546}
547
548#[inline(always)]
549pub(crate) fn exec_sample(t: &Thunk, base: *mut u8) {
550    let Thunk::Sample {
551        logits,
552        dst,
553        batch,
554        vocab,
555        top_k,
556        top_p,
557        temperature,
558        seed,
559    } = t
560    else {
561        unreachable!()
562    };
563    unsafe {
564        execute_sample_f32(
565            *logits,
566            *dst,
567            *batch as usize,
568            *vocab as usize,
569            *top_k as usize,
570            *top_p,
571            *temperature,
572            *seed,
573            base,
574        );
575    }
576}
577
578#[inline(always)]
579pub(crate) fn exec_reduce(t: &Thunk, base: *mut u8) {
580    let Thunk::Reduce {
581        src,
582        dst,
583        outer,
584        reduced,
585        inner,
586        op,
587    } = t
588    else {
589        unreachable!()
590    };
591    {
592        let outer = *outer as usize;
593        let reduced = *reduced as usize;
594        let inner = *inner as usize;
595        let in_total = outer * reduced * inner;
596        let out_total = outer * inner;
597        unsafe {
598            let inp = sl(*src, base, in_total);
599            let out = sl_mut(*dst, base, out_total);
600            // Each output element reduces a disjoint strided strip, so
601            // the output range parallelizes (the bias-gradient reductions
602            // read the big [N,C,H,W] tensors down to [C]; that's C-way).
603            let reduce_one = |oi: usize| -> f32 {
604                let o = oi / inner;
605                let i = oi % inner;
606                let mut acc = match op {
607                    ReduceOp::Max => f32::NEG_INFINITY,
608                    ReduceOp::Min => f32::INFINITY,
609                    ReduceOp::Prod => 1.0f32,
610                    _ => 0.0f32, // Sum / Mean
611                };
612                for r in 0..reduced {
613                    let v = inp[o * reduced * inner + r * inner + i];
614                    acc = match op {
615                        ReduceOp::Sum | ReduceOp::Mean => acc + v,
616                        ReduceOp::Max => acc.max(v),
617                        ReduceOp::Min => acc.min(v),
618                        ReduceOp::Prod => acc * v,
619                    };
620                }
621                if matches!(op, ReduceOp::Mean) {
622                    acc /= reduced as f32;
623                }
624                acc
625            };
626            if fast_conv_enabled() && crate::pool::should_parallelize(in_total) && out_total > 1 {
627                let out_addr = out.as_mut_ptr() as usize;
628                crate::pool::par_for(
629                    out_total,
630                    crate::pool::outer_chunk(out_total),
631                    &|off, cnt| {
632                        for oi in off..off + cnt {
633                            *((out_addr as *mut f32).add(oi)) = reduce_one(oi);
634                        }
635                    },
636                );
637            } else {
638                for oi in 0..out_total {
639                    out[oi] = reduce_one(oi);
640                }
641            }
642        }
643    }
644}
645
646#[inline(always)]
647pub(crate) fn exec_arg_reduce(t: &Thunk, base: *mut u8) {
648    let Thunk::ArgReduce {
649        src,
650        dst,
651        outer,
652        reduced,
653        inner,
654        is_max,
655    } = t
656    else {
657        unreachable!()
658    };
659    {
660        let outer = *outer as usize;
661        let reduced = *reduced as usize;
662        let inner = *inner as usize;
663        let in_total = outer * reduced * inner;
664        let out_total = outer * inner;
665        unsafe {
666            let inp = sl(*src, base, in_total);
667            let out = sl_mut(*dst, base, out_total);
668            for o in 0..outer {
669                for i in 0..inner {
670                    let mut best = inp[o * reduced * inner + i];
671                    let mut best_idx = 0usize;
672                    for r in 1..reduced {
673                        let v = inp[o * reduced * inner + r * inner + i];
674                        let better = if *is_max { v > best } else { v < best };
675                        if better {
676                            best = v;
677                            best_idx = r;
678                        }
679                    }
680                    out[o * inner + i] = best_idx as f32;
681                }
682            }
683        }
684    }
685}
686
687#[inline(always)]
688pub(crate) fn exec_cumsum_backward(t: &Thunk, base: *mut u8) {
689    let Thunk::CumsumBackward {
690        dy,
691        dx,
692        rows,
693        cols,
694        exclusive,
695    } = t
696    else {
697        unreachable!()
698    };
699    {
700        let (rows, cols) = (*rows as usize, *cols as usize);
701        unsafe {
702            let dys = sl(*dy, base, rows * cols);
703            let out = sl_mut(*dx, base, rows * cols);
704            for r in 0..rows {
705                crate::training_bwd::cumsum_backward_row(
706                    &dys[r * cols..(r + 1) * cols],
707                    &mut out[r * cols..(r + 1) * cols],
708                    *exclusive,
709                );
710            }
711        }
712    }
713}
714
715#[inline(always)]
716pub(crate) fn exec_softmax_cross_entropy_dense(t: &Thunk, base: *mut u8) {
717    let Thunk::SoftmaxCrossEntropyDense {
718        logits,
719        targets,
720        dst,
721        n,
722        c,
723    } = t
724    else {
725        unreachable!()
726    };
727    {
728        let n = *n as usize;
729        let c = *c as usize;
730        unsafe {
731            let lg = sl(*logits, base, n * c);
732            let tg = sl(*targets, base, n * c);
733            let out = sl_mut(*dst, base, n);
734            for ni in 0..n {
735                let row = &lg[ni * c..(ni + 1) * c];
736                let trow = &tg[ni * c..(ni + 1) * c];
737                // log-sum-exp: max-subtract for stability.
738                let mut m = f32::NEG_INFINITY;
739                for &v in row {
740                    if v > m {
741                        m = v;
742                    }
743                }
744                let mut sum = 0f32;
745                for &v in row {
746                    sum += (v - m).exp();
747                }
748                let lse = m + sum.ln();
749                // loss = lse - Σ_c targets[c]·logits[c].
750                let mut dot = 0f32;
751                for k in 0..c {
752                    dot += trow[k] * row[k];
753                }
754                out[ni] = lse - dot;
755            }
756        }
757    }
758}
759
760#[inline(always)]
761pub(crate) fn exec_softmax_cross_entropy(t: &Thunk, base: *mut u8) {
762    let Thunk::SoftmaxCrossEntropy {
763        logits,
764        labels,
765        dst,
766        n,
767        c,
768    } = t
769    else {
770        unreachable!()
771    };
772    {
773        let n = *n as usize;
774        let c = *c as usize;
775        unsafe {
776            let lg = sl(*logits, base, n * c);
777            let lb = sl(*labels, base, n);
778            let out = sl_mut(*dst, base, n);
779            for ni in 0..n {
780                let row = &lg[ni * c..(ni + 1) * c];
781                // log-sum-exp: max-subtract for stability.
782                let mut m = f32::NEG_INFINITY;
783                for &v in row {
784                    if v > m {
785                        m = v;
786                    }
787                }
788                let mut sum = 0f32;
789                for &v in row {
790                    sum += (v - m).exp();
791                }
792                let lse = m + sum.ln();
793                let label_idx = lb[ni] as usize;
794                // loss = -(logits[label] - lse) = lse - logits[label].
795                out[ni] = lse - row[label_idx];
796            }
797        }
798    }
799}
800
801#[inline(always)]
802pub(crate) fn exec_softmax_cross_entropy_backward(t: &Thunk, base: *mut u8) {
803    let Thunk::SoftmaxCrossEntropyBackward {
804        logits,
805        labels,
806        d_loss,
807        dlogits,
808        n,
809        c,
810    } = t
811    else {
812        unreachable!()
813    };
814    {
815        let n = *n as usize;
816        let c = *c as usize;
817        unsafe {
818            let lg = sl(*logits, base, n * c);
819            let lb = sl(*labels, base, n);
820            let dl = sl(*d_loss, base, n);
821            let out = sl_mut(*dlogits, base, n * c);
822            for ni in 0..n {
823                let row = &lg[ni * c..(ni + 1) * c];
824                let label_idx = lb[ni] as usize;
825                let scale = dl[ni];
826                let mut m = f32::NEG_INFINITY;
827                for &v in row {
828                    if v > m {
829                        m = v;
830                    }
831                }
832                let mut sum = 0f32;
833                for &v in row {
834                    sum += (v - m).exp();
835                }
836                let inv_sum = 1.0 / sum;
837                let dst_row = &mut out[ni * c..(ni + 1) * c];
838                for k in 0..c {
839                    let p = (row[k] - m).exp() * inv_sum;
840                    let one_hot = if k == label_idx { 1.0 } else { 0.0 };
841                    dst_row[k] = (p - one_hot) * scale;
842                }
843            }
844        }
845    }
846}
847
848/// Reference for `Op::ArgMax`/`Op::ArgMin` (f32-encoded indices, first-hit
849/// tie-break). Reduces the middle `reduced` axis: input is logically
850/// `[outer, reduced, inner]`, output `[outer, inner]`. Shared by the CPU thunk
851/// and the Metal/WGPU host paths.
852pub unsafe fn execute_argreduce_f32(
853    src: usize,
854    dst: usize,
855    outer: usize,
856    reduced: usize,
857    inner: usize,
858    is_max: bool,
859    base: *mut u8,
860) {
861    let bptr = base as usize;
862    unsafe {
863        let inp = std::slice::from_raw_parts((bptr + src) as *const f32, outer * reduced * inner);
864        let out = std::slice::from_raw_parts_mut((bptr + dst) as *mut f32, outer * inner);
865        for o in 0..outer {
866            for i in 0..inner {
867                let mut best = inp[o * reduced * inner + i];
868                let mut best_idx = 0usize;
869                for r in 1..reduced {
870                    let v = inp[o * reduced * inner + r * inner + i];
871                    let better = if is_max { v > best } else { v < best };
872                    if better {
873                        best = v;
874                        best_idx = r;
875                    }
876                }
877                out[o * inner + i] = best_idx as f32;
878            }
879        }
880    }
881}
882
883/// Token-sampling reference. Shared by the CPU `Thunk::Sample` arm and the
884/// Metal unified-memory host fallback. `logits` is `[batch, vocab]` f32;
885/// writes one f32-encoded index per batch row to `dst`. With a fixed `seed`
886/// (and same top_k/top_p/temperature) the result is deterministic.
887pub unsafe fn execute_sample_f32(
888    logits: usize,
889    dst: usize,
890    batch: usize,
891    vocab: usize,
892    top_k: usize,
893    top_p: f32,
894    temperature: f32,
895    seed: u64,
896    base: *mut u8,
897) {
898    let (b, v) = (batch, vocab);
899    let k = top_k.min(v);
900    unsafe {
901        let lg = sl(logits, base, b * v);
902        let out = sl_mut(dst, base, b);
903        let mut rng = rlx_ir::Philox4x32::new(if seed == 0 { 0xDEADBEEF } else { seed });
904        for bi in 0..b {
905            let row = &lg[bi * v..(bi + 1) * v];
906            out[bi] = sample_row(row, k, top_p, temperature, &mut rng) as f32;
907        }
908    }
909}
910
911pub unsafe fn execute_cumsum_backward_f32(
912    dy: usize,
913    dx: usize,
914    rows: u32,
915    cols: u32,
916    exclusive: bool,
917    base: *mut u8,
918) {
919    let (rows, cols) = (rows as usize, cols as usize);
920    let dys = sl(dy, base, rows * cols);
921    let out = sl_mut(dx, base, rows * cols);
922    for r in 0..rows {
923        crate::training_bwd::cumsum_backward_row(
924            &dys[r * cols..(r + 1) * cols],
925            &mut out[r * cols..(r + 1) * cols],
926            exclusive,
927        );
928    }
929}
930
931/// f64 sum reduction over a contiguous middle range.
932/// Layout: input is `[outer, reduced, inner]`, output is `[outer, inner]`.
933pub(crate) fn reduce_sum_f64(
934    inp: &[f64],
935    out: &mut [f64],
936    outer: usize,
937    reduced: usize,
938    inner: usize,
939) {
940    for o in 0..outer {
941        for n in 0..inner {
942            let mut acc = 0.0_f64;
943            for r in 0..reduced {
944                acc += inp[o * reduced * inner + r * inner + n];
945            }
946            out[o * inner + n] = acc;
947        }
948    }
949}
950
951/// Host-side RNG fill against a byte arena (Metal/CUDA unified-memory fallback).
952///
953/// # Safety
954///
955/// `arena` must point to a valid allocation with at least `dst_off + len * 4` bytes.
956pub unsafe fn fill_rng_normal_arena(
957    dst_off: usize,
958    len: usize,
959    mean: f32,
960    scale: f32,
961    key: u64,
962    op_seed: Option<f32>,
963    opts: rlx_ir::RngOptions,
964    arena: *mut u8,
965) {
966    if len == 0 {
967        return;
968    }
969    unsafe {
970        let out = std::slice::from_raw_parts_mut((arena.add(dst_off)) as *mut f32, len);
971        rlx_ir::fill_normal_like(out, mean, scale, opts, key, op_seed);
972    }
973}
974
975pub unsafe fn fill_rng_uniform_arena(
976    dst_off: usize,
977    len: usize,
978    low: f32,
979    high: f32,
980    key: u64,
981    op_seed: Option<f32>,
982    opts: rlx_ir::RngOptions,
983    arena: *mut u8,
984) {
985    if len == 0 {
986        return;
987    }
988    unsafe {
989        let out = std::slice::from_raw_parts_mut((arena.add(dst_off)) as *mut f32, len);
990        rlx_ir::fill_uniform_like(out, low, high, opts, key, op_seed);
991    }
992}