Skip to main content

ftts_kernels/
f32ref.rs

1//! f32 reference kernels: the correctness baseline every optimized tier must reproduce.
2//!
3//! These are deliberately the obvious implementations. They exist so that a SIMD or int8 kernel has
4//! something bit-comparable to be judged against (G1 > G2 — parity first, speed second), and so the
5//! first end-to-end forward can be brought up without any unsafe at all. Nothing here is on the hot
6//! path yet; nothing here should be "optimized" in place. When a fast tier lands it lands beside
7//! these, with a test asserting the two agree.
8//!
9//! Accumulation is f32 to match the reference stack's CPU fp32 tier. In particular, RMSNorm widens
10//! BF16 inputs to f32 and accumulates its variance in f32, exactly as the resolved QK-Norm contract
11//! requires.
12
13/// Reduction order used by [`linear_with_accumulation`].
14///
15/// The scalar order is the f32 reference used by production code. The lane orders are retained so
16/// the CPU-fp32 fixture test can identify whether a BLAS-style partial reduction is responsible
17/// for a layer-level arithmetic divergence.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum F32LinearAccumulation {
20    /// One left-to-right f32 accumulator.
21    Scalar,
22    /// Four independent f32 partial accumulators, reduced in lane order.
23    Lanes4,
24    /// Eight independent f32 partial accumulators, reduced in lane order.
25    Lanes8,
26    /// Four FMA partial accumulators, reduced in lane order.
27    FusedLanes4,
28    /// Eight FMA partial accumulators, reduced in lane order.
29    FusedLanes8,
30    /// macOS Accelerate SGEMM, selected only by the CPU-fp32 parity harness.
31    ///
32    /// On every other target, this deliberately falls back to [`Self::Scalar`].
33    Accelerate,
34    /// [`Self::Accelerate`], with M = 1 calls pinned onto the M >= 2 GEMM kernel.
35    ///
36    /// See [`Self::AccelerateBiasSeededRowInvariant`] for the streaming == offline rationale;
37    /// this is the same pinning for the `beta = 0` route (the codec's RVQ projections).
38    AccelerateRowInvariant,
39    /// macOS Accelerate SGEMM over a bias-seeded output, issued with `beta = 1`.
40    ///
41    /// This is the exact call `slow_conv2d_update_output_frame` makes for a convolution with a
42    /// bias, and it differs from [`Self::Accelerate`] — which adds the bias after a `beta = 0`
43    /// product — whenever the BLAS blocks its reduction. Like the other lane orders, it exists so
44    /// the CPU-fp32 fixture can attribute a convolution's divergence; it falls back to
45    /// [`Self::Scalar`] with a trailing bias on every non-macOS target.
46    AccelerateBiasSeeded,
47    /// [`Self::AccelerateBiasSeeded`], with M = 1 calls pinned onto the M >= 2 GEMM kernel.
48    ///
49    /// Accelerate routes M = 1 to a GEMV kernel whose reduction order differs from its (measured
50    /// row-invariant) M >= 2 GEMM kernel. Seams whose streaming variant must equal whole-sequence
51    /// decode bit-for-bit — the codec convolutions — need every M on the same kernel path, and
52    /// accept drifting a single-frame call away from the oracle's own GEMV bits to get it. Seams
53    /// the ORACLE itself computes at M = 1 (the speaker-encoder embedding head) must NOT use
54    /// this: the GEMV path is the oracle-matching one there.
55    AccelerateBiasSeededRowInvariant,
56    /// One f64 accumulator, narrowed to f32 only at the store.
57    ///
58    /// Not a candidate for what the oracle did — it is an *attribution probe*. Every f32 lane
59    /// order above is one guess at the oracle's reduction; this one removes the reduction's
60    /// rounding entirely, so the residual it leaves at a seam is the part of that seam's
61    /// divergence that a reduction order cannot explain. See `talker_layer_attribution`.
62    WidenedF64,
63}
64
65/// Arithmetic used by [`rms_norm_with_arithmetic`] to form RMSNorm's scale.
66///
67/// The scalar reciprocal-square-root path is the f32 reference used by production code. The other
68/// modes make the exact CPU-fp32 fixture able to discriminate reduction precision and reciprocal
69/// placement without changing that normal path.
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub enum F32RmsNormArithmetic {
72    /// Left-to-right f32 reduction and `sqrt(value).recip()`.
73    ScalarReciprocalSqrt,
74    /// Left-to-right f32 reduction and `1.0 / sqrt(value)`.
75    ScalarDivideSqrt,
76    /// Four f32 partial sums, then `sqrt(value).recip()`.
77    Lanes4ReciprocalSqrt,
78    /// Eight f32 partial sums, then `sqrt(value).recip()`.
79    Lanes8ReciprocalSqrt,
80    /// Sixteen f32 partial sums, then `sqrt(value).recip()`.
81    Lanes16ReciprocalSqrt,
82    /// Thirty-two f32 partial sums, then `sqrt(value).recip()`.
83    Lanes32ReciprocalSqrt,
84    /// The reference stack's own cascade reduction over a **4-wide** vector, then
85    /// `sqrt(value).recip()`. See [`torch_cascade_sum`].
86    TorchCascade4ReciprocalSqrt,
87    /// The reference stack's cascade reduction over an **8-wide** vector — the width an ARM build
88    /// with `AT_BUILD_ARM_VEC256_WITH_SLEEF` uses, which the pinned oracle reports.
89    TorchCascade8ReciprocalSqrt,
90    /// f64 reduction and scale calculation, narrowed only at the final scale.
91    F64ReciprocalSqrt,
92}
93
94impl F32RmsNormArithmetic {
95    /// The variant that removes this operation's f32 reduction rounding, for attribution probes.
96    pub const WIDENED_F64: Self = Self::F64ReciprocalSqrt;
97}
98
99/// Association used by [`silu_mul_in_place_with_arithmetic`].
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub enum F32SiluArithmetic {
102    /// `x / (1 + exp(-x))`.
103    Divide,
104    /// `x * (1 / (1 + exp(-x)))`, matching `x * sigmoid(x)` association.
105    MultiplyReciprocal,
106    /// The whole expression in f64, narrowed only at the store — an attribution probe, not a
107    /// candidate for what the oracle did.
108    WidenedF64,
109}
110
111/// Normalization form used by [`softmax_rows_with_arithmetic`].
112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113pub enum F32SoftmaxArithmetic {
114    /// Form one reciprocal then multiply every exponent by it.
115    ReciprocalMultiply,
116    /// Divide every exponent by the sum directly.
117    Divide,
118    /// Exponentiate, sum and normalize in f64, narrowing only at the store — an attribution
119    /// probe, not a candidate for what the oracle did.
120    WidenedF64,
121}
122
123/// Row-major matrix-vector/matrix-matrix product in the layout PyTorch `Linear` stores.
124///
125/// `x` is `[m, k]`, `weight` is `[n, k]` (out-features major, as `nn.Linear` stores it), and the
126/// result is `[m, n]`. Bias is optional because every attention/MLP projection in this model is
127/// bias-free; only `text_projection` carries one.
128///
129/// # Panics
130///
131/// Panics if any slice length disagrees with `m`, `k`, `n`.
132pub fn linear(
133    x: &[f32],
134    weight: &[f32],
135    bias: Option<&[f32]>,
136    m: usize,
137    k: usize,
138    n: usize,
139    out: &mut [f32],
140) {
141    linear_with_accumulation(x, weight, bias, m, k, n, F32LinearAccumulation::Scalar, out);
142}
143
144/// Same operation as [`linear`], with an explicitly chosen f32 dot-product reduction order.
145///
146/// This exists for parity forensics. The normal [`linear`] entry point remains the scalar,
147/// left-to-right reference.
148#[allow(clippy::too_many_arguments)]
149pub fn linear_with_accumulation(
150    x: &[f32],
151    weight: &[f32],
152    bias: Option<&[f32]>,
153    m: usize,
154    k: usize,
155    n: usize,
156    accumulation: F32LinearAccumulation,
157    out: &mut [f32],
158) {
159    assert_eq!(x.len(), m * k, "x must be [m, k]");
160    assert_eq!(weight.len(), n * k, "weight must be [n, k]");
161    assert_eq!(out.len(), m * n, "out must be [m, n]");
162    if let Some(bias) = bias {
163        assert_eq!(bias.len(), n, "bias must be [n]");
164    }
165
166    if matches!(
167        accumulation,
168        F32LinearAccumulation::AccelerateBiasSeeded
169            | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
170    ) {
171        // Seed `out` with the bias and let the BLAS accumulate onto it, exactly as the reference
172        // convolution's `beta = 1` GEMM does.
173        match bias {
174            Some(bias) => {
175                for row in out.chunks_exact_mut(n) {
176                    row.copy_from_slice(bias);
177                }
178            }
179            None => out.fill(0.0),
180        }
181        let row_invariant = accumulation == F32LinearAccumulation::AccelerateBiasSeededRowInvariant;
182        if accelerate_sgemm(x, weight, m, k, n, 1.0, row_invariant, out) {
183            return;
184        }
185        out.fill(0.0);
186    }
187
188    if matches!(
189        accumulation,
190        F32LinearAccumulation::Accelerate | F32LinearAccumulation::AccelerateRowInvariant
191    ) && accelerate_sgemm(
192        x,
193        weight,
194        m,
195        k,
196        n,
197        0.0,
198        accumulation == F32LinearAccumulation::AccelerateRowInvariant,
199        out,
200    ) {
201        if let Some(bias) = bias {
202            for row in out.chunks_exact_mut(n) {
203                for (value, offset) in row.iter_mut().zip(bias) {
204                    *value += offset;
205                }
206            }
207        }
208        return;
209    }
210
211    for row in 0..m {
212        let x_row = &x[row * k..row * k + k];
213        for col in 0..n {
214            let w_row = &weight[col * k..col * k + k];
215            let sum = dot_with_accumulation(x_row, w_row, accumulation);
216            out[row * n + col] = bias.map_or(sum, |b| sum + b[col]);
217        }
218    }
219}
220
221fn dot_with_accumulation(x: &[f32], weight: &[f32], accumulation: F32LinearAccumulation) -> f32 {
222    assert_eq!(x.len(), weight.len(), "dot-product inputs must match");
223    match accumulation {
224        F32LinearAccumulation::Scalar => {
225            let mut sum = 0.0f32;
226            for index in 0..x.len() {
227                sum += x[index] * weight[index];
228            }
229            sum
230        }
231        F32LinearAccumulation::WidenedF64 => {
232            let mut sum = 0.0f64;
233            for index in 0..x.len() {
234                sum += f64::from(x[index]) * f64::from(weight[index]);
235            }
236            sum as f32
237        }
238        F32LinearAccumulation::Lanes4
239        | F32LinearAccumulation::Lanes8
240        | F32LinearAccumulation::FusedLanes4
241        | F32LinearAccumulation::FusedLanes8
242        | F32LinearAccumulation::Accelerate
243        | F32LinearAccumulation::AccelerateRowInvariant
244        | F32LinearAccumulation::AccelerateBiasSeeded
245        | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => {
246            let lanes = match accumulation {
247                F32LinearAccumulation::Lanes4 => 4,
248                F32LinearAccumulation::Lanes8 => 8,
249                F32LinearAccumulation::FusedLanes4 => 4,
250                F32LinearAccumulation::FusedLanes8 => 8,
251                F32LinearAccumulation::Accelerate
252                | F32LinearAccumulation::AccelerateRowInvariant
253                | F32LinearAccumulation::AccelerateBiasSeeded
254                | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => 1,
255                F32LinearAccumulation::Scalar | F32LinearAccumulation::WidenedF64 => {
256                    unreachable!("scalar and widened orders are handled above")
257                }
258            };
259            let mut partial = [0.0f32; 8];
260            for index in 0..x.len() {
261                let lane = index % lanes;
262                partial[lane] = match accumulation {
263                    F32LinearAccumulation::FusedLanes4 | F32LinearAccumulation::FusedLanes8 => {
264                        x[index].mul_add(weight[index], partial[lane])
265                    }
266                    F32LinearAccumulation::Scalar
267                    | F32LinearAccumulation::Lanes4
268                    | F32LinearAccumulation::Lanes8
269                    | F32LinearAccumulation::Accelerate
270                    | F32LinearAccumulation::AccelerateRowInvariant
271                    | F32LinearAccumulation::AccelerateBiasSeeded
272                    | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
273                    | F32LinearAccumulation::WidenedF64 => partial[lane] + x[index] * weight[index],
274                };
275            }
276            let mut sum = 0.0f32;
277            for value in &partial[..lanes] {
278                sum += *value;
279            }
280            sum
281        }
282    }
283}
284
285/// Attempts the same row-major SGEMM backend recorded by the pinned macOS CPU-fp32 oracle.
286///
287/// The test-only [`F32LinearAccumulation::Accelerate`] selector keeps this foreign call out of the
288/// normal safe-Rust reference path. Targets without the opt-in macOS backend return `false`, so
289/// their scalar fallback remains bit-for-bit the ordinary reference implementation.
290#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
291#[allow(clippy::too_many_arguments)]
292fn accelerate_sgemm(
293    x: &[f32],
294    weight: &[f32],
295    m: usize,
296    k: usize,
297    n: usize,
298    beta: f32,
299    row_invariant: bool,
300    out: &mut [f32],
301) -> bool {
302    // Accelerate routes M = 1 to a GEMV kernel whose reduction order differs from its M >= 2
303    // GEMM kernel in the last ulps, while M >= 2 is row-invariant (measured: M of 2, 3, 4 and 14
304    // produce bit-identical rows). A streaming decode presents the same convolution at M = packet
305    // while offline presents M = utterance, so without this pinning the streaming == offline gate
306    // fails on every 1-frame packet. Under `row_invariant`, present M = 1 as a duplicated-row
307    // M = 2 call and keep row 0, so every M reduces on the same kernel path. Seams the ORACLE
308    // itself computes at M = 1 (the speaker-encoder embedding head) must NOT request this:
309    // the GEMV bits ARE the oracle's bits there.
310    if row_invariant && m == 1 {
311        let mut doubled_x = Vec::with_capacity(2 * k);
312        doubled_x.extend_from_slice(x);
313        doubled_x.extend_from_slice(x);
314        let mut doubled_out = Vec::with_capacity(2 * n);
315        doubled_out.extend_from_slice(out);
316        doubled_out.extend_from_slice(out);
317        if !accelerate_sgemm(&doubled_x, weight, 2, k, n, beta, false, &mut doubled_out) {
318            return false;
319        }
320        out.copy_from_slice(&doubled_out[..n]);
321        return true;
322    }
323    let m = i32::try_from(m).expect("SGEMM rows fit CBLAS i32 dimensions");
324    let k = i32::try_from(k).expect("SGEMM reduction fits CBLAS i32 dimensions");
325    let n = i32::try_from(n).expect("SGEMM columns fit CBLAS i32 dimensions");
326    // SAFETY: `linear_with_accumulation` proves the row-major slice lengths before this call.
327    // `x` is M×K, `weight` is N×K and is passed transposed, and `out` is M×N. All pointers remain
328    // valid and non-overlapping for the full synchronous CBLAS call.
329    unsafe {
330        cblas_sgemm(
331            CBLAS_ROW_MAJOR,
332            CBLAS_NO_TRANSPOSE,
333            CBLAS_TRANSPOSE,
334            m,
335            n,
336            k,
337            1.0,
338            x.as_ptr(),
339            k,
340            weight.as_ptr(),
341            k,
342            beta,
343            out.as_mut_ptr(),
344            n,
345        );
346    }
347    true
348}
349
350#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
351fn accelerate_sgemm(
352    _x: &[f32],
353    _weight: &[f32],
354    _m: usize,
355    _k: usize,
356    _n: usize,
357    _beta: f32,
358    _row_invariant: bool,
359    _out: &mut [f32],
360) -> bool {
361    false
362}
363
364#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
365const CBLAS_ROW_MAJOR: i32 = 101;
366#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
367const CBLAS_NO_TRANSPOSE: i32 = 111;
368#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
369const CBLAS_TRANSPOSE: i32 = 112;
370
371#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
372#[link(name = "Accelerate", kind = "framework")]
373unsafe extern "C" {
374    fn cblas_sgemm(
375        order: i32,
376        trans_a: i32,
377        trans_b: i32,
378        m: i32,
379        n: i32,
380        k: i32,
381        alpha: f32,
382        a: *const f32,
383        lda: i32,
384        b: *const f32,
385        ldb: i32,
386        beta: f32,
387        c: *mut f32,
388        ldc: i32,
389    );
390}
391
392/// Which `sin`/`exp` implementation an elementwise parity probe evaluates.
393///
394/// The reference stack does not call the scalar libm for large tensors: its CPU elementwise kernels
395/// dispatch through a vectorized `Vectorized<float>`, whose transcendentals are ~1-ulp routines
396/// rather than correctly-rounded ones. `codec_snake_bisect` established that the SnakeBeta seam's
397/// entire residual divergence lives in exactly these two functions — every other operation in that
398/// expression is a correctly-rounded f32 `*`, `+` or `/` with no freedom at all — so identifying
399/// *which* vectorized routine the pinned oracle used is the whole remaining question there.
400#[derive(Clone, Copy, Debug, Eq, PartialEq)]
401pub enum F32Transcendental {
402    /// Rust's scalar `f32::sin` / `f32::exp`, i.e. the platform libm. The production reference.
403    ScalarLibm,
404    /// macOS Accelerate vForce (`vvsinf` / `vvexpf`), selected only by the parity harness.
405    ///
406    /// On every other target this deliberately falls back to [`Self::ScalarLibm`].
407    AccelerateVForce,
408    /// SLEEF's 1-ulp `Sleef_sinf_u10` / `Sleef_expf_u10`, ported to safe Rust in [`crate::sleef`].
409    ///
410    /// This is the routine an AArch64 `Vectorized<float>` actually dispatches to, so it is the
411    /// candidate the vForce probe was only ever standing in for — and unlike vForce it is portable
412    /// and could therefore be adopted into production if it measures exact.
413    SleefU10,
414}
415
416/// Fills `out` with `sin(x)` under the selected implementation.
417///
418/// # Panics
419///
420/// Panics if `out` is not the same length as `x`.
421pub fn sin_with(x: &[f32], implementation: F32Transcendental, out: &mut [f32]) {
422    assert_eq!(x.len(), out.len(), "sin output must match its input");
423    if implementation == F32Transcendental::AccelerateVForce && vforce_sin(x, out) {
424        return;
425    }
426    if implementation == F32Transcendental::SleefU10 {
427        for (value, target) in x.iter().zip(out.iter_mut()) {
428            *target = crate::sleef::sinf_u10(*value);
429        }
430        return;
431    }
432    for (value, target) in x.iter().zip(out.iter_mut()) {
433        *target = value.sin();
434    }
435}
436
437/// Fills `out` with `exp(x)` under the selected implementation.
438///
439/// # Panics
440///
441/// Panics if `out` is not the same length as `x`.
442pub fn exp_with(x: &[f32], implementation: F32Transcendental, out: &mut [f32]) {
443    assert_eq!(x.len(), out.len(), "exp output must match its input");
444    if implementation == F32Transcendental::AccelerateVForce && vforce_exp(x, out) {
445        return;
446    }
447    if implementation == F32Transcendental::SleefU10 {
448        for (value, target) in x.iter().zip(out.iter_mut()) {
449            *target = crate::sleef::expf_u10(*value);
450        }
451        return;
452    }
453    for (value, target) in x.iter().zip(out.iter_mut()) {
454        *target = value.exp();
455    }
456}
457
458#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
459fn vforce_sin(x: &[f32], out: &mut [f32]) -> bool {
460    let count = i32::try_from(x.len()).expect("vForce length fits i32");
461    // SAFETY: `sin_with` proved the two slices have equal length, and they are distinct live
462    // allocations for the duration of this synchronous call.
463    unsafe { vvsinf(out.as_mut_ptr(), x.as_ptr(), &raw const count) };
464    true
465}
466
467#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
468fn vforce_exp(x: &[f32], out: &mut [f32]) -> bool {
469    let count = i32::try_from(x.len()).expect("vForce length fits i32");
470    // SAFETY: as in `vforce_sin`.
471    unsafe { vvexpf(out.as_mut_ptr(), x.as_ptr(), &raw const count) };
472    true
473}
474
475#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
476fn vforce_sin(_x: &[f32], _out: &mut [f32]) -> bool {
477    false
478}
479
480#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
481fn vforce_exp(_x: &[f32], _out: &mut [f32]) -> bool {
482    false
483}
484
485#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
486#[link(name = "Accelerate", kind = "framework")]
487unsafe extern "C" {
488    fn vvsinf(out: *mut f32, x: *const f32, count: *const i32);
489    fn vvexpf(out: *mut f32, x: *const f32, count: *const i32);
490}
491
492/// Qwen3 RMSNorm: `x * rsqrt(mean(x^2) + eps) * weight`, weight-only, no centering.
493///
494/// # Panics
495///
496/// Panics if `x` is not `rows * dim` elements or `weight` is not `dim`.
497pub fn rms_norm(x: &[f32], weight: &[f32], eps: f32, rows: usize, dim: usize, out: &mut [f32]) {
498    rms_norm_with_arithmetic(
499        x,
500        weight,
501        eps,
502        rows,
503        dim,
504        F32RmsNormArithmetic::ScalarReciprocalSqrt,
505        out,
506    );
507}
508
509/// Same operation as [`rms_norm`], with an explicitly selected reduction and scale calculation.
510///
511/// This entry point is for CPU-fp32 parity forensics; [`rms_norm`] remains the normal scalar f32
512/// reference path.
513pub fn rms_norm_with_arithmetic(
514    x: &[f32],
515    weight: &[f32],
516    eps: f32,
517    rows: usize,
518    dim: usize,
519    arithmetic: F32RmsNormArithmetic,
520    out: &mut [f32],
521) {
522    assert_eq!(x.len(), rows * dim, "x must be [rows, dim]");
523    assert_eq!(weight.len(), dim, "weight must be [dim]");
524    assert_eq!(out.len(), rows * dim, "out must be [rows, dim]");
525
526    for row in 0..rows {
527        let src = &x[row * dim..row * dim + dim];
528        let scale = rms_scale(src, eps, arithmetic);
529        for index in 0..dim {
530            out[row * dim + index] = src[index] * scale * weight[index];
531        }
532    }
533}
534
535fn rms_scale(src: &[f32], eps: f32, arithmetic: F32RmsNormArithmetic) -> f32 {
536    match arithmetic {
537        F32RmsNormArithmetic::ScalarReciprocalSqrt => {
538            let sum = sum_squares_f32(src, 1);
539            (sum / src.len() as f32 + eps).sqrt().recip()
540        }
541        F32RmsNormArithmetic::ScalarDivideSqrt => {
542            let sum = sum_squares_f32(src, 1);
543            1.0f32 / (sum / src.len() as f32 + eps).sqrt()
544        }
545        F32RmsNormArithmetic::Lanes4ReciprocalSqrt => {
546            let sum = sum_squares_f32(src, 4);
547            (sum / src.len() as f32 + eps).sqrt().recip()
548        }
549        F32RmsNormArithmetic::Lanes8ReciprocalSqrt => {
550            let sum = sum_squares_f32(src, 8);
551            (sum / src.len() as f32 + eps).sqrt().recip()
552        }
553        F32RmsNormArithmetic::Lanes16ReciprocalSqrt => {
554            let sum = sum_squares_f32(src, 16);
555            (sum / src.len() as f32 + eps).sqrt().recip()
556        }
557        F32RmsNormArithmetic::Lanes32ReciprocalSqrt => {
558            let sum = sum_squares_f32(src, 32);
559            (sum / src.len() as f32 + eps).sqrt().recip()
560        }
561        F32RmsNormArithmetic::TorchCascade4ReciprocalSqrt => {
562            let sum = torch_cascade_sum(src, 4, |value| value * value);
563            (sum / src.len() as f32 + eps).sqrt().recip()
564        }
565        F32RmsNormArithmetic::TorchCascade8ReciprocalSqrt => {
566            let sum = torch_cascade_sum(src, 8, |value| value * value);
567            (sum / src.len() as f32 + eps).sqrt().recip()
568        }
569        F32RmsNormArithmetic::F64ReciprocalSqrt => {
570            let mut sum = 0.0f64;
571            for value in src {
572                let value = f64::from(*value);
573                sum += value * value;
574            }
575            (sum / src.len() as f64 + f64::from(eps)).sqrt().recip() as f32
576        }
577    }
578}
579
580fn sum_squares_f32(src: &[f32], lanes: usize) -> f32 {
581    let mut partial = [0.0f32; 32];
582    for (index, value) in src.iter().enumerate() {
583        partial[index % lanes] += *value * *value;
584    }
585    let mut sum = 0.0f32;
586    for value in &partial[..lanes] {
587        sum += *value;
588    }
589    sum
590}
591
592/// The reference stack's contiguous-inner-dimension f32 sum, transcribed operation for operation.
593///
594/// Every other reduction offered here is a *guess* at the oracle's order — "four accumulators
595/// landed closer, so perhaps it used four". This one is not a guess: it is the shape PyTorch's
596/// `SumKernel.cpp` actually reduces with, and it is materially different from any flat lane
597/// interleave, so a flat lane order that lands close can still never land on it.
598///
599/// Three nested structures, outermost first:
600///
601/// 1. **Vector lanes.** The row is walked `width` elements at a time and each lane keeps its own
602///    running sum. `width` is `Vectorized<float>::size()`, 8 on an ARM build with
603///    `AT_BUILD_ARM_VEC256_WITH_SLEEF` and 4 without, which is why it is a parameter here rather
604///    than a constant: it is the one part of the shape the provenance does not pin.
605/// 2. **Instruction-level parallelism.** `row_sum` splits the vector stream into `ILP = 4`
606///    independent chains, reduced `p0 += p1; p0 += p2; p0 += p3` at the end.
607/// 3. **Cascade.** Each chain is not a flat running sum but a `LEVELS = 4` deep cascade: level 0
608///    absorbs `level_step` vectors and is then drained into level 1, level 1 into level 2 when its
609///    own counter wraps, and so on. This is what bounds the reduction's error growth, and it makes
610///    the partial-sum magnitudes — and therefore the rounding — differ from a flat sum even at
611///    identical lane counts.
612///
613/// The final horizontal fold is left-to-right over the lanes, as `vectorized_inner_sum` does when
614/// it stores the accumulator and sums the array.
615///
616/// `transform` is applied to each element before accumulation, so RMSNorm's `pow(2).mean(-1)` can
617/// square in the same pass the reference's separate `pow(2)` tensor would have.
618///
619/// # Panics
620///
621/// Panics if `width` is zero.
622// Index loops keep the reference's exact accumulation order (level, chain, lane) visible;
623// iterator rewrites would obscure the summation-order argument this port is documenting.
624#[allow(clippy::needless_range_loop)]
625pub fn torch_cascade_sum(src: &[f32], width: usize, transform: impl Fn(f32) -> f32) -> f32 {
626    assert!(width > 0, "vector width must be positive");
627    const ILP: usize = 4;
628    const LEVELS: usize = 4;
629
630    let vector_count = src.len() / width;
631    let vector = |index: usize, lane: usize| transform(src[index * width + lane]);
632
633    // `multi_row_sum(in_data, row_stride = col_stride * ILP, col_stride, size = vector_count / ILP)`
634    let size = vector_count / ILP;
635    let level_power = ceil_log2(size).div_euclid(LEVELS).max(4);
636    let level_step = 1usize << level_power;
637    let level_mask = level_step - 1;
638
639    let mut acc = vec![[0.0f32; ILP].map(|_| vec![0.0f32; width]); LEVELS];
640    let mut index = 0usize;
641    while index + level_step <= size {
642        for _ in 0..level_step {
643            for chain in 0..ILP {
644                for lane in 0..width {
645                    acc[0][chain][lane] += vector(index * ILP + chain, lane);
646                }
647            }
648            index += 1;
649        }
650        for level in 1..LEVELS {
651            for chain in 0..ILP {
652                for lane in 0..width {
653                    acc[level][chain][lane] += acc[level - 1][chain][lane];
654                    acc[level - 1][chain][lane] = 0.0;
655                }
656            }
657            if index & (level_mask << (level * level_power)) != 0 {
658                break;
659            }
660        }
661    }
662    while index < size {
663        for chain in 0..ILP {
664            for lane in 0..width {
665                acc[0][chain][lane] += vector(index * ILP + chain, lane);
666            }
667        }
668        index += 1;
669    }
670    for level in 1..LEVELS {
671        for chain in 0..ILP {
672            for lane in 0..width {
673                acc[level][chain][lane] += acc[level - 1][chain][lane];
674            }
675        }
676    }
677
678    // `row_sum`: absorb the vectors `multi_row_sum` could not group, then fold the ILP chains.
679    let mut partial = acc.swap_remove(LEVELS - 1);
680    for leftover in size * ILP..vector_count {
681        for lane in 0..width {
682            partial[0][lane] += vector(leftover, lane);
683        }
684    }
685    for chain in 1..ILP {
686        for lane in 0..width {
687            partial[0][lane] += partial[chain][lane];
688        }
689    }
690
691    // `vectorized_inner_sum`: the elements past the last whole vector are summed first, then the
692    // lanes are folded into that running total left to right.
693    let mut sum = 0.0f32;
694    for index in vector_count * width..src.len() {
695        sum += transform(src[index]);
696    }
697    for lane in 0..width {
698        sum += partial[0][lane];
699    }
700    sum
701}
702
703/// `c10::llvm::CeilLog2` for the sizes this reduction sees: `ceil(log2(value))`, zero for `0` and `1`.
704fn ceil_log2(value: usize) -> usize {
705    if value <= 1 {
706        return 0;
707    }
708    usize::BITS as usize - (value - 1).leading_zeros() as usize
709}
710
711/// SwiGLU's elementwise half: `silu(gate) * up`, written into `gate`.
712pub fn silu_mul_in_place(gate: &mut [f32], up: &[f32]) {
713    silu_mul_in_place_with_arithmetic(gate, up, F32SiluArithmetic::Divide);
714}
715
716/// Same operation as [`silu_mul_in_place`], with an explicitly chosen f32 association.
717pub fn silu_mul_in_place_with_arithmetic(
718    gate: &mut [f32],
719    up: &[f32],
720    arithmetic: F32SiluArithmetic,
721) {
722    assert_eq!(gate.len(), up.len(), "gate and up must match");
723    for (g, u) in gate.iter_mut().zip(up) {
724        let x = *g;
725        if arithmetic == F32SiluArithmetic::WidenedF64 {
726            let wide = f64::from(x);
727            *g = (wide / (1.0 + (-wide).exp()) * f64::from(*u)) as f32;
728            continue;
729        }
730        let denominator = 1.0 + (-x).exp();
731        let silu = match arithmetic {
732            F32SiluArithmetic::Divide => x / denominator,
733            F32SiluArithmetic::MultiplyReciprocal => x * denominator.recip(),
734            F32SiluArithmetic::WidenedF64 => unreachable!("handled above"),
735        };
736        *g = silu * u;
737    }
738}
739
740/// In-place row-wise softmax in f32, max-subtracted for stability.
741pub fn softmax_rows(x: &mut [f32], rows: usize, cols: usize) {
742    softmax_rows_with_arithmetic(x, rows, cols, F32SoftmaxArithmetic::ReciprocalMultiply);
743}
744
745/// Same operation as [`softmax_rows`], with an explicitly selected normalization form.
746pub fn softmax_rows_with_arithmetic(
747    x: &mut [f32],
748    rows: usize,
749    cols: usize,
750    arithmetic: F32SoftmaxArithmetic,
751) {
752    assert_eq!(x.len(), rows * cols, "x must be [rows, cols]");
753    for row in 0..rows {
754        let slice = &mut x[row * cols..row * cols + cols];
755        let mut max = f32::NEG_INFINITY;
756        for value in slice.iter() {
757            if *value > max {
758                max = *value;
759            }
760        }
761        if arithmetic == F32SoftmaxArithmetic::WidenedF64 {
762            let max = f64::from(max);
763            let mut wide = Vec::with_capacity(slice.len());
764            let mut sum = 0.0f64;
765            for value in slice.iter() {
766                let exponent = (f64::from(*value) - max).exp();
767                sum += exponent;
768                wide.push(exponent);
769            }
770            for (value, exponent) in slice.iter_mut().zip(wide) {
771                *value = (exponent / sum) as f32;
772            }
773            continue;
774        }
775        let mut sum = 0.0f32;
776        for value in slice.iter_mut() {
777            *value = (*value - max).exp();
778            sum += *value;
779        }
780        for value in slice.iter_mut() {
781            *value = match arithmetic {
782                F32SoftmaxArithmetic::ReciprocalMultiply => *value * sum.recip(),
783                F32SoftmaxArithmetic::Divide => *value / sum,
784                F32SoftmaxArithmetic::WidenedF64 => unreachable!("handled above"),
785            };
786        }
787    }
788}
789
790/// Grouped-query attention for row-major f32 tensors.
791///
792/// `queries` and `out` are `[query_positions, q_heads, head_dim]`; `keys` and `values` are
793/// `[key_positions, kv_heads, head_dim]`; `additive_mask` is `[query_positions, key_positions]`.
794/// Query head `h` reads key/value head `h / (q_heads / kv_heads)`, matching Qwen3-TTS's 16 query
795/// heads over 8 KV heads. The reduction order is scalar and fixed so an ISA-specific kernel has a
796/// direct f32 reference to compare against.
797///
798/// # Panics
799///
800/// Panics if the dimensions disagree or query heads are not evenly grouped over KV heads.
801#[allow(clippy::too_many_arguments)]
802pub fn gqa_attention(
803    queries: &[f32],
804    keys: &[f32],
805    values: &[f32],
806    additive_mask: &[f32],
807    query_positions: usize,
808    key_positions: usize,
809    q_heads: usize,
810    kv_heads: usize,
811    head_dim: usize,
812    out: &mut [f32],
813) {
814    gqa_attention_with_softmax(
815        queries,
816        keys,
817        values,
818        additive_mask,
819        query_positions,
820        key_positions,
821        q_heads,
822        kv_heads,
823        head_dim,
824        F32SoftmaxArithmetic::ReciprocalMultiply,
825        out,
826    );
827}
828
829/// Same operation as [`gqa_attention`], with an explicitly selected softmax normalization form.
830#[allow(clippy::too_many_arguments)]
831pub fn gqa_attention_with_softmax(
832    queries: &[f32],
833    keys: &[f32],
834    values: &[f32],
835    additive_mask: &[f32],
836    query_positions: usize,
837    key_positions: usize,
838    q_heads: usize,
839    kv_heads: usize,
840    head_dim: usize,
841    softmax_arithmetic: F32SoftmaxArithmetic,
842    out: &mut [f32],
843) {
844    gqa_attention_with_arithmetic(
845        queries,
846        keys,
847        values,
848        additive_mask,
849        query_positions,
850        key_positions,
851        q_heads,
852        kv_heads,
853        head_dim,
854        softmax_arithmetic,
855        F32LinearAccumulation::Scalar,
856        out,
857    );
858}
859
860/// Same operation as [`gqa_attention`], with selected softmax and dot-product reduction forms.
861#[allow(clippy::too_many_arguments)]
862pub fn gqa_attention_with_arithmetic(
863    queries: &[f32],
864    keys: &[f32],
865    values: &[f32],
866    additive_mask: &[f32],
867    query_positions: usize,
868    key_positions: usize,
869    q_heads: usize,
870    kv_heads: usize,
871    head_dim: usize,
872    softmax_arithmetic: F32SoftmaxArithmetic,
873    accumulation: F32LinearAccumulation,
874    out: &mut [f32],
875) {
876    assert!(kv_heads > 0, "at least one KV head is required");
877    assert_eq!(
878        q_heads % kv_heads,
879        0,
880        "query heads must divide evenly into KV groups"
881    );
882    assert_eq!(
883        queries.len(),
884        query_positions * q_heads * head_dim,
885        "queries must be [query_positions, q_heads, head_dim]"
886    );
887    assert_eq!(
888        keys.len(),
889        key_positions * kv_heads * head_dim,
890        "keys must be [key_positions, kv_heads, head_dim]"
891    );
892    assert_eq!(
893        values.len(),
894        key_positions * kv_heads * head_dim,
895        "values must be [key_positions, kv_heads, head_dim]"
896    );
897    assert_eq!(
898        additive_mask.len(),
899        query_positions * key_positions,
900        "mask must be [query_positions, key_positions]"
901    );
902    assert_eq!(
903        out.len(),
904        query_positions * q_heads * head_dim,
905        "out must be [query_positions, q_heads, head_dim]"
906    );
907
908    if accumulation == F32LinearAccumulation::Accelerate
909        && accelerate_gqa_attention(
910            queries,
911            keys,
912            values,
913            additive_mask,
914            query_positions,
915            key_positions,
916            q_heads,
917            kv_heads,
918            head_dim,
919            softmax_arithmetic,
920            out,
921        )
922    {
923        return;
924    }
925
926    let scale = (head_dim as f32).sqrt().recip();
927    let kv_group = q_heads / kv_heads;
928    let mut scores = vec![0.0f32; key_positions];
929
930    for query_position in 0..query_positions {
931        let mask =
932            &additive_mask[query_position * key_positions..(query_position + 1) * key_positions];
933        for q_head in 0..q_heads {
934            let kv_head = q_head / kv_group;
935            let query_base = (query_position * q_heads + q_head) * head_dim;
936            let query = &queries[query_base..query_base + head_dim];
937
938            for (key_position, score) in scores.iter_mut().enumerate() {
939                let key_base = (key_position * kv_heads + kv_head) * head_dim;
940                let key = &keys[key_base..key_base + head_dim];
941                let dot = dot_with_accumulation(query, key, accumulation);
942                *score = dot * scale + mask[key_position];
943            }
944            softmax_rows_with_arithmetic(&mut scores, 1, key_positions, softmax_arithmetic);
945
946            let out_base = query_base;
947            attention_weighted_sum(
948                &scores,
949                values,
950                kv_head,
951                kv_heads,
952                head_dim,
953                accumulation,
954                &mut out[out_base..out_base + head_dim],
955            );
956        }
957    }
958}
959
960/// Executes the two attention matrix products through the exact macOS SGEMM candidate.
961///
962/// This is intentionally an L2-parity probe rather than the normal attention route. The gather
963/// buffers present the strided GQA heads as the row-major matrices consumed by CBLAS; the scalar
964/// path above remains the cross-platform reference and the fallback when this candidate is off.
965#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
966#[allow(clippy::too_many_arguments)]
967fn accelerate_gqa_attention(
968    queries: &[f32],
969    keys: &[f32],
970    values: &[f32],
971    additive_mask: &[f32],
972    query_positions: usize,
973    key_positions: usize,
974    q_heads: usize,
975    kv_heads: usize,
976    head_dim: usize,
977    softmax_arithmetic: F32SoftmaxArithmetic,
978    out: &mut [f32],
979) -> bool {
980    let scale = (head_dim as f32).sqrt().recip();
981    let kv_group = q_heads / kv_heads;
982    let mut query_matrix = vec![0.0f32; query_positions * head_dim];
983    let mut key_matrix = vec![0.0f32; key_positions * head_dim];
984    let mut value_transpose = vec![0.0f32; head_dim * key_positions];
985    let mut scores = vec![0.0f32; query_positions * key_positions];
986    let mut context = vec![0.0f32; query_positions * head_dim];
987
988    for q_head in 0..q_heads {
989        let kv_head = q_head / kv_group;
990        for query_position in 0..query_positions {
991            let query_base = (query_position * q_heads + q_head) * head_dim;
992            query_matrix[query_position * head_dim..(query_position + 1) * head_dim]
993                .copy_from_slice(&queries[query_base..query_base + head_dim]);
994        }
995        for key_position in 0..key_positions {
996            let key_base = (key_position * kv_heads + kv_head) * head_dim;
997            key_matrix[key_position * head_dim..(key_position + 1) * head_dim]
998                .copy_from_slice(&keys[key_base..key_base + head_dim]);
999            for lane in 0..head_dim {
1000                value_transpose[lane * key_positions + key_position] = values[key_base + lane];
1001            }
1002        }
1003
1004        if !accelerate_sgemm(
1005            &query_matrix,
1006            &key_matrix,
1007            query_positions,
1008            head_dim,
1009            key_positions,
1010            0.0,
1011            false,
1012            &mut scores,
1013        ) {
1014            return false;
1015        }
1016        for query_position in 0..query_positions {
1017            let score_row =
1018                &mut scores[query_position * key_positions..(query_position + 1) * key_positions];
1019            let mask = &additive_mask
1020                [query_position * key_positions..(query_position + 1) * key_positions];
1021            for (score, mask_value) in score_row.iter_mut().zip(mask) {
1022                *score = *score * scale + mask_value;
1023            }
1024            softmax_rows_with_arithmetic(score_row, 1, key_positions, softmax_arithmetic);
1025        }
1026        if !accelerate_sgemm(
1027            &scores,
1028            &value_transpose,
1029            query_positions,
1030            key_positions,
1031            head_dim,
1032            0.0,
1033            false,
1034            &mut context,
1035        ) {
1036            return false;
1037        }
1038        for query_position in 0..query_positions {
1039            let out_base = (query_position * q_heads + q_head) * head_dim;
1040            out[out_base..out_base + head_dim].copy_from_slice(
1041                &context[query_position * head_dim..(query_position + 1) * head_dim],
1042            );
1043        }
1044    }
1045    true
1046}
1047
1048#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
1049#[allow(clippy::too_many_arguments)]
1050fn accelerate_gqa_attention(
1051    _queries: &[f32],
1052    _keys: &[f32],
1053    _values: &[f32],
1054    _additive_mask: &[f32],
1055    _query_positions: usize,
1056    _key_positions: usize,
1057    _q_heads: usize,
1058    _kv_heads: usize,
1059    _head_dim: usize,
1060    _softmax_arithmetic: F32SoftmaxArithmetic,
1061    _out: &mut [f32],
1062) -> bool {
1063    false
1064}
1065
1066#[allow(clippy::too_many_arguments)]
1067fn attention_weighted_sum(
1068    scores: &[f32],
1069    values: &[f32],
1070    kv_head: usize,
1071    kv_heads: usize,
1072    head_dim: usize,
1073    accumulation: F32LinearAccumulation,
1074    out: &mut [f32],
1075) {
1076    if accumulation == F32LinearAccumulation::WidenedF64 {
1077        for lane in 0..head_dim {
1078            let mut sum = 0.0f64;
1079            for (key_position, weight) in scores.iter().copied().enumerate() {
1080                let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1081                sum += f64::from(weight) * f64::from(value);
1082            }
1083            out[lane] = sum as f32;
1084        }
1085        return;
1086    }
1087    let lanes = match accumulation {
1088        F32LinearAccumulation::Scalar => 1,
1089        F32LinearAccumulation::Lanes4 | F32LinearAccumulation::FusedLanes4 => 4,
1090        F32LinearAccumulation::Lanes8 | F32LinearAccumulation::FusedLanes8 => 8,
1091        F32LinearAccumulation::Accelerate
1092        | F32LinearAccumulation::AccelerateRowInvariant
1093        | F32LinearAccumulation::AccelerateBiasSeeded
1094        | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => 1,
1095        F32LinearAccumulation::WidenedF64 => unreachable!("handled above"),
1096    };
1097    for lane in 0..head_dim {
1098        let mut partial = [0.0f32; 8];
1099        for (key_position, weight) in scores.iter().copied().enumerate() {
1100            let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1101            let partial_index = key_position % lanes;
1102            partial[partial_index] = match accumulation {
1103                F32LinearAccumulation::FusedLanes4 | F32LinearAccumulation::FusedLanes8 => {
1104                    weight.mul_add(value, partial[partial_index])
1105                }
1106                F32LinearAccumulation::Scalar
1107                | F32LinearAccumulation::Lanes4
1108                | F32LinearAccumulation::Lanes8
1109                | F32LinearAccumulation::Accelerate
1110                | F32LinearAccumulation::AccelerateRowInvariant
1111                | F32LinearAccumulation::AccelerateBiasSeeded
1112                | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
1113                | F32LinearAccumulation::WidenedF64 => partial[partial_index] + weight * value,
1114            };
1115        }
1116        let mut sum = 0.0f32;
1117        for value in &partial[..lanes] {
1118            sum += *value;
1119        }
1120        out[lane] = sum;
1121    }
1122}
1123
1124/// Collapse the three mRoPE axes into one `cos`/`sin` row using the checkpoint's INTERLEAVED rule.
1125///
1126/// The pinned config sets `rope_scaling.interleaved = true`, which selects a different branch from
1127/// the familiar section-split one — a difference that is numerically invisible whenever the three
1128/// axes carry equal positions (which OQ-4 says they always do here, all three receiving the same
1129/// scalar causal index) and therefore exactly the kind of thing a port gets wrong and only discovers
1130/// against a batched or genuinely multimodal input. It is implemented faithfully regardless.
1131///
1132/// `axes` is the first half of each axis's row, `[3][half]`; `out` receives `[half]`. Element `j`
1133/// takes axis `j % 3` while `j` lies in `1..sections[1..].max() * 3`, and axis 0 elsewhere.
1134///
1135/// # Panics
1136///
1137/// Panics if `out` is not `half` long or an axis row is short.
1138pub fn mrope_interleave(axes: [&[f32]; 3], sections: [usize; 3], out: &mut [f32]) {
1139    let half = out.len();
1140    for axis in axes {
1141        assert!(
1142            axis.len() >= half,
1143            "axis row shorter than the half-dimension"
1144        );
1145    }
1146
1147    // Start from axis 0 everywhere, then overwrite the strided lanes from axes 1 and 2, exactly as
1148    // the reference does with its `x_t[..., beg:end:3] = x[beg, ..., beg:end:3]` assignments.
1149    out.copy_from_slice(&axes[0][..half]);
1150    let modality_num = 3usize;
1151    for (axis_index, section) in sections.iter().enumerate().skip(1) {
1152        let end = section * modality_num;
1153        let mut lane = axis_index;
1154        while lane < end && lane < half {
1155            out[lane] = axes[axis_index][lane];
1156            lane += modality_num;
1157        }
1158    }
1159}
1160
1161/// Apply rotary embeddings to one head row in the `rotate_half` layout.
1162///
1163/// `row` is `[head_dim]`; `cos` and `sin` are the full `[head_dim]` rows (the doubled half). The
1164/// transform is `x*cos + rotate_half(x)*sin` where `rotate_half` maps `[a, b] -> [-b, a]` over the
1165/// two halves.
1166///
1167/// # Panics
1168///
1169/// Panics if `cos`/`sin` do not match `row`, or if `head_dim` is odd.
1170pub fn apply_rope_in_place(row: &mut [f32], cos: &[f32], sin: &[f32]) {
1171    let dim = row.len();
1172    assert_eq!(cos.len(), dim, "cos must match head_dim");
1173    assert_eq!(sin.len(), dim, "sin must match head_dim");
1174    assert!(dim.is_multiple_of(2), "head_dim must be even");
1175
1176    let half = dim / 2;
1177    let original: Vec<f32> = row.to_vec();
1178    for index in 0..dim {
1179        let rotated = if index < half {
1180            -original[index + half]
1181        } else {
1182            original[index - half]
1183        };
1184        row[index] = original[index] * cos[index] + rotated * sin[index];
1185    }
1186}
1187
1188#[cfg(test)]
1189mod tests {
1190    use super::*;
1191
1192    #[test]
1193    fn linear_matches_a_hand_computed_product() {
1194        // x = [[1, 2, 3]], weight = [[1, 0, -1], [2, 2, 2]] -> [1*1 + 2*0 + 3*-1, 2+4+6] = [-2, 12]
1195        let x = [1.0, 2.0, 3.0];
1196        let weight = [1.0, 0.0, -1.0, 2.0, 2.0, 2.0];
1197        let mut out = [0.0; 2];
1198        linear(&x, &weight, None, 1, 3, 2, &mut out);
1199        assert_eq!(out, [-2.0, 12.0]);
1200
1201        let mut biased = [0.0; 2];
1202        linear(&x, &weight, Some(&[10.0, -12.0]), 1, 3, 2, &mut biased);
1203        assert_eq!(biased, [8.0, 0.0]);
1204    }
1205
1206    #[test]
1207    fn torch_cascade_sum_agrees_with_the_flat_sum_when_rounding_cannot_intervene() {
1208        // Powers of two below 2^24 add without rounding, so every reduction order must produce the
1209        // same total. This proves the cascade's bookkeeping — its drains, its ILP chains, its tail
1210        // handling — visits each element exactly once, independently of any parity claim.
1211        for length in [1usize, 7, 8, 15, 16, 31, 128, 1024, 3072] {
1212            for width in [4usize, 8] {
1213                let values: Vec<f32> = (0..length).map(|index| (index % 8) as f32).collect();
1214                let flat: f32 = values.iter().sum();
1215                assert_eq!(
1216                    torch_cascade_sum(&values, width, |value| value),
1217                    flat,
1218                    "length {length}, width {width}"
1219                );
1220            }
1221        }
1222    }
1223
1224    #[test]
1225    fn torch_cascade_sum_applies_its_transform_before_accumulating() {
1226        let values = [1.0f32, 2.0, 3.0, 4.0, 5.0];
1227        assert_eq!(torch_cascade_sum(&values, 4, |value| value * value), 55.0);
1228    }
1229
1230    #[test]
1231    fn torch_cascade_sum_differs_from_a_flat_sum_once_rounding_matters() {
1232        // A large leading term followed by many small ones is exactly the case a cascade exists to
1233        // improve: the flat sum loses every small term into the large accumulator, the cascade does
1234        // not. If these ever agreed, the transcription would have collapsed into a flat sum and the
1235        // parity sweep would be comparing one order against itself.
1236        let mut values = vec![1.0f32; 1024];
1237        values[0] = 1.0e8;
1238        let flat = values.iter().fold(0.0f32, |sum, value| sum + value);
1239        assert_ne!(torch_cascade_sum(&values, 8, |value| value), flat);
1240    }
1241
1242    #[test]
1243    fn ceil_log2_matches_its_definition() {
1244        assert_eq!(ceil_log2(0), 0);
1245        assert_eq!(ceil_log2(1), 0);
1246        assert_eq!(ceil_log2(2), 1);
1247        assert_eq!(ceil_log2(3), 2);
1248        assert_eq!(ceil_log2(32), 5);
1249        assert_eq!(ceil_log2(33), 6);
1250    }
1251
1252    #[test]
1253    fn rms_norm_normalizes_and_scales() {
1254        // mean(x^2) for [3, 4] is 12.5; rsqrt(12.5 + 0) ~ 0.2828427
1255        let x = [3.0f32, 4.0];
1256        let weight = [1.0f32, 1.0];
1257        let mut out = [0.0; 2];
1258        rms_norm(&x, &weight, 0.0, 1, 2, &mut out);
1259        let expected = 12.5f32.sqrt().recip();
1260        assert!((out[0] - 3.0 * expected).abs() < 1e-6);
1261        assert!((out[1] - 4.0 * expected).abs() < 1e-6);
1262
1263        // The weight is applied per element, after scaling.
1264        let mut weighted = [0.0; 2];
1265        rms_norm(&x, &[2.0, 0.5], 0.0, 1, 2, &mut weighted);
1266        assert!((weighted[0] - 3.0 * expected * 2.0).abs() < 1e-6);
1267        assert!((weighted[1] - 4.0 * expected * 0.5).abs() < 1e-6);
1268    }
1269
1270    #[test]
1271    fn silu_mul_matches_the_definition() {
1272        let mut gate = [0.0f32, 1.0, -1.0];
1273        let up = [1.0f32, 2.0, 3.0];
1274        silu_mul_in_place(&mut gate, &up);
1275        assert_eq!(gate[0], 0.0);
1276        let silu_one = 1.0f32 / (1.0 + (-1.0f32).exp());
1277        assert!((gate[1] - silu_one * 2.0).abs() < 1e-6);
1278        let silu_neg = -1.0f32 / (1.0 + 1.0f32.exp());
1279        assert!((gate[2] - silu_neg * 3.0).abs() < 1e-6);
1280    }
1281
1282    #[test]
1283    fn softmax_rows_sums_to_one_and_is_shift_invariant() {
1284        let mut x = [1.0f32, 2.0, 3.0, 101.0, 102.0, 103.0];
1285        softmax_rows(&mut x, 2, 3);
1286        let first: f32 = x[..3].iter().sum();
1287        let second: f32 = x[3..].iter().sum();
1288        assert!((first - 1.0).abs() < 1e-6);
1289        assert!((second - 1.0).abs() < 1e-6);
1290        // Rows differing by a constant shift must produce identical distributions.
1291        for index in 0..3 {
1292            assert!((x[index] - x[index + 3]).abs() < 1e-6);
1293        }
1294    }
1295
1296    #[test]
1297    fn gqa_maps_each_query_head_to_its_kv_group() {
1298        let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (1, 1, 4, 2, 2);
1299        let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1300        let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1301        let values = [10.0f32, 11.0, 20.0, 21.0];
1302        let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1303
1304        gqa_attention(
1305            &queries,
1306            &keys,
1307            &values,
1308            &[0.0],
1309            query_positions,
1310            key_positions,
1311            q_heads,
1312            kv_heads,
1313            head_dim,
1314            &mut out,
1315        );
1316
1317        assert_eq!(&out[0..2], &[10.0, 11.0]);
1318        assert_eq!(&out[2..4], &[10.0, 11.0]);
1319        assert_eq!(&out[4..6], &[20.0, 21.0]);
1320        assert_eq!(&out[6..8], &[20.0, 21.0]);
1321    }
1322
1323    #[test]
1324    fn gqa_honors_the_additive_causal_mask() {
1325        let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (2, 2, 1, 1, 2);
1326        let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1327        let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1328        let values = [2.0f32, 4.0, 10.0, 20.0];
1329        let mask = [0.0f32, f32::NEG_INFINITY, 0.0, 0.0];
1330        let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1331
1332        gqa_attention(
1333            &queries,
1334            &keys,
1335            &values,
1336            &mask,
1337            query_positions,
1338            key_positions,
1339            q_heads,
1340            kv_heads,
1341            head_dim,
1342            &mut out,
1343        );
1344
1345        assert_eq!(&out[0..2], &[2.0, 4.0]);
1346        assert_eq!(&out[2..4], &[6.0, 12.0]);
1347    }
1348
1349    #[test]
1350    fn rope_rotates_a_known_pair() {
1351        // head_dim 2, cos = [0, 0], sin = [1, 1]: [a, b] -> [-b, a]
1352        let mut row = [3.0f32, 5.0];
1353        apply_rope_in_place(&mut row, &[0.0, 0.0], &[1.0, 1.0]);
1354        assert_eq!(row, [-5.0, 3.0]);
1355
1356        // Identity when cos = 1, sin = 0.
1357        let mut same = [3.0f32, 5.0];
1358        apply_rope_in_place(&mut same, &[1.0, 1.0], &[0.0, 0.0]);
1359        assert_eq!(same, [3.0, 5.0]);
1360    }
1361
1362    #[test]
1363    fn mrope_interleave_is_identity_when_all_axes_agree() {
1364        // OQ-4: all three axes carry the same scalar causal index in this model, so the interleave
1365        // must be a no-op on equal axes. If it is not, the lane arithmetic is wrong.
1366        let axis: Vec<f32> = (0..64).map(|value| value as f32).collect();
1367        let mut out = vec![0.0f32; 64];
1368        mrope_interleave([&axis, &axis, &axis], [24, 20, 20], &mut out);
1369        assert_eq!(out, axis);
1370    }
1371
1372    #[test]
1373    fn mrope_interleave_selects_the_documented_lanes() {
1374        let zeros = vec![0.0f32; 64];
1375        let ones = vec![1.0f32; 64];
1376        let twos = vec![2.0f32; 64];
1377        let mut out = vec![0.0f32; 64];
1378        mrope_interleave([&zeros, &ones, &twos], [24, 20, 20], &mut out);
1379
1380        // Lanes 1, 4, .. < 60 come from axis 1; lanes 2, 5, .. < 60 from axis 2; the rest stay 0,
1381        // including every lane at or above 60.
1382        for (lane, value) in out.iter().enumerate() {
1383            let expected = if lane < 60 && lane % 3 == 1 {
1384                1.0
1385            } else if lane < 60 && lane % 3 == 2 {
1386                2.0
1387            } else {
1388                0.0
1389            };
1390            assert_eq!(*value, expected, "lane {lane}");
1391        }
1392    }
1393}