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    gqa_attention_head_range_with_arithmetic(
927        queries,
928        keys,
929        values,
930        additive_mask,
931        query_positions,
932        key_positions,
933        q_heads,
934        kv_heads,
935        head_dim,
936        softmax_arithmetic,
937        accumulation,
938        0..q_heads,
939        out,
940    );
941}
942
943/// The scalar GQA loop restricted to `q_head_range`, writing only those heads' output spans.
944///
945/// This is the SAME loop [`gqa_attention_with_arithmetic`] runs — extracted, not duplicated —
946/// so a partitioned caller (the worker team) composes the identical arithmetic per head and the
947/// full-range serial call remains the reference. Heads are independent: no reduction crosses a
948/// head, which is why partitioning here is bit-exact rather than merely close.
949///
950/// # Panics
951///
952/// Panics if the range exceeds `q_heads`. Full shape validation is the full-range caller's job;
953/// partitioned callers must have validated once before splitting.
954#[allow(clippy::too_many_arguments)]
955pub fn gqa_attention_head_range_with_arithmetic(
956    queries: &[f32],
957    keys: &[f32],
958    values: &[f32],
959    additive_mask: &[f32],
960    query_positions: usize,
961    key_positions: usize,
962    q_heads: usize,
963    kv_heads: usize,
964    head_dim: usize,
965    softmax_arithmetic: F32SoftmaxArithmetic,
966    accumulation: F32LinearAccumulation,
967    q_head_range: std::ops::Range<usize>,
968    out: &mut [f32],
969) {
970    assert!(
971        out.len() >= query_positions * q_heads * head_dim,
972        "attention output must hold [query_positions, q_heads, head_dim]"
973    );
974    let out = out.as_mut_ptr();
975    // SAFETY: the pointer comes from the `&mut` slice above, whose length was just checked to
976    // cover every index the head range can reach, and it is not used after this call.
977    unsafe {
978        gqa_attention_head_range_into(
979            queries,
980            keys,
981            values,
982            additive_mask,
983            query_positions,
984            key_positions,
985            q_heads,
986            kv_heads,
987            head_dim,
988            softmax_arithmetic,
989            accumulation,
990            q_head_range,
991            out,
992        );
993    }
994}
995
996/// The head-range attention loop, writing through a raw output pointer.
997///
998/// This exists so parallel workers never have to materialize a `&mut [f32]` over the whole output
999/// while a sibling worker holds one too. Disjoint *writes* are not enough for that to be sound:
1000/// two live `&mut` into the same allocation is undefined behaviour whatever the access pattern,
1001/// and `rustc` marks `&mut` parameters `noalias`, so it is the optimizer — not just the model —
1002/// that the overlap would mislead. Here each worker turns the pointer into a `&mut` covering
1003/// exactly the one head span it is about to write, and those spans are disjoint by construction.
1004///
1005/// # Safety
1006///
1007/// `out` must be valid for writes across `[query_positions, q_heads, head_dim]`, and no other
1008/// reference may alias the `head_dim` spans this call's `q_head_range` writes for its duration.
1009#[allow(clippy::too_many_arguments)]
1010pub(crate) unsafe fn gqa_attention_head_range_into(
1011    queries: &[f32],
1012    keys: &[f32],
1013    values: &[f32],
1014    additive_mask: &[f32],
1015    query_positions: usize,
1016    key_positions: usize,
1017    q_heads: usize,
1018    kv_heads: usize,
1019    head_dim: usize,
1020    softmax_arithmetic: F32SoftmaxArithmetic,
1021    accumulation: F32LinearAccumulation,
1022    q_head_range: std::ops::Range<usize>,
1023    out: *mut f32,
1024) {
1025    assert!(q_head_range.end <= q_heads, "head range exceeds q_heads");
1026    let scale = (head_dim as f32).sqrt().recip();
1027    let kv_group = q_heads / kv_heads;
1028    let mut scores = vec![0.0f32; key_positions];
1029
1030    for query_position in 0..query_positions {
1031        let mask =
1032            &additive_mask[query_position * key_positions..(query_position + 1) * key_positions];
1033        for q_head in q_head_range.clone() {
1034            let kv_head = q_head / kv_group;
1035            let query_base = (query_position * q_heads + q_head) * head_dim;
1036            let query = &queries[query_base..query_base + head_dim];
1037            for (key_position, score) in scores.iter_mut().enumerate() {
1038                let key_base = (key_position * kv_heads + kv_head) * head_dim;
1039                let key = &keys[key_base..key_base + head_dim];
1040                let dot = dot_with_accumulation(query, key, accumulation);
1041                *score = dot * scale + mask[key_position];
1042            }
1043            softmax_rows_with_arithmetic(&mut scores, 1, key_positions, softmax_arithmetic);
1044
1045            // SAFETY: `query_base` indexes [query_position, q_head, head_dim] inside the bounds
1046            // the caller guaranteed, and this borrow spans only this head — the one span this
1047            // partition owns, disjoint from every other partition's.
1048            let head_out = unsafe { std::slice::from_raw_parts_mut(out.add(query_base), head_dim) };
1049            attention_weighted_sum(
1050                &scores,
1051                values,
1052                kv_head,
1053                kv_heads,
1054                head_dim,
1055                accumulation,
1056                head_out,
1057            );
1058        }
1059    }
1060}
1061
1062/// Executes the two attention matrix products through the exact macOS SGEMM candidate.
1063///
1064/// This is intentionally an L2-parity probe rather than the normal attention route. The gather
1065/// buffers present the strided GQA heads as the row-major matrices consumed by CBLAS; the scalar
1066/// path above remains the cross-platform reference and the fallback when this candidate is off.
1067#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
1068#[allow(clippy::too_many_arguments)]
1069fn accelerate_gqa_attention(
1070    queries: &[f32],
1071    keys: &[f32],
1072    values: &[f32],
1073    additive_mask: &[f32],
1074    query_positions: usize,
1075    key_positions: usize,
1076    q_heads: usize,
1077    kv_heads: usize,
1078    head_dim: usize,
1079    softmax_arithmetic: F32SoftmaxArithmetic,
1080    out: &mut [f32],
1081) -> bool {
1082    let scale = (head_dim as f32).sqrt().recip();
1083    let kv_group = q_heads / kv_heads;
1084    let mut query_matrix = vec![0.0f32; query_positions * head_dim];
1085    let mut key_matrix = vec![0.0f32; key_positions * head_dim];
1086    let mut value_transpose = vec![0.0f32; head_dim * key_positions];
1087    let mut scores = vec![0.0f32; query_positions * key_positions];
1088    let mut context = vec![0.0f32; query_positions * head_dim];
1089
1090    for q_head in 0..q_heads {
1091        let kv_head = q_head / kv_group;
1092        for query_position in 0..query_positions {
1093            let query_base = (query_position * q_heads + q_head) * head_dim;
1094            query_matrix[query_position * head_dim..(query_position + 1) * head_dim]
1095                .copy_from_slice(&queries[query_base..query_base + head_dim]);
1096        }
1097        for key_position in 0..key_positions {
1098            let key_base = (key_position * kv_heads + kv_head) * head_dim;
1099            key_matrix[key_position * head_dim..(key_position + 1) * head_dim]
1100                .copy_from_slice(&keys[key_base..key_base + head_dim]);
1101            for lane in 0..head_dim {
1102                value_transpose[lane * key_positions + key_position] = values[key_base + lane];
1103            }
1104        }
1105
1106        if !accelerate_sgemm(
1107            &query_matrix,
1108            &key_matrix,
1109            query_positions,
1110            head_dim,
1111            key_positions,
1112            0.0,
1113            false,
1114            &mut scores,
1115        ) {
1116            return false;
1117        }
1118        for query_position in 0..query_positions {
1119            let score_row =
1120                &mut scores[query_position * key_positions..(query_position + 1) * key_positions];
1121            let mask = &additive_mask
1122                [query_position * key_positions..(query_position + 1) * key_positions];
1123            for (score, mask_value) in score_row.iter_mut().zip(mask) {
1124                *score = *score * scale + mask_value;
1125            }
1126            softmax_rows_with_arithmetic(score_row, 1, key_positions, softmax_arithmetic);
1127        }
1128        if !accelerate_sgemm(
1129            &scores,
1130            &value_transpose,
1131            query_positions,
1132            key_positions,
1133            head_dim,
1134            0.0,
1135            false,
1136            &mut context,
1137        ) {
1138            return false;
1139        }
1140        for query_position in 0..query_positions {
1141            let out_base = (query_position * q_heads + q_head) * head_dim;
1142            out[out_base..out_base + head_dim].copy_from_slice(
1143                &context[query_position * head_dim..(query_position + 1) * head_dim],
1144            );
1145        }
1146    }
1147    true
1148}
1149
1150#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
1151#[allow(clippy::too_many_arguments)]
1152fn accelerate_gqa_attention(
1153    _queries: &[f32],
1154    _keys: &[f32],
1155    _values: &[f32],
1156    _additive_mask: &[f32],
1157    _query_positions: usize,
1158    _key_positions: usize,
1159    _q_heads: usize,
1160    _kv_heads: usize,
1161    _head_dim: usize,
1162    _softmax_arithmetic: F32SoftmaxArithmetic,
1163    _out: &mut [f32],
1164) -> bool {
1165    false
1166}
1167
1168#[allow(clippy::too_many_arguments)]
1169fn attention_weighted_sum(
1170    scores: &[f32],
1171    values: &[f32],
1172    kv_head: usize,
1173    kv_heads: usize,
1174    head_dim: usize,
1175    accumulation: F32LinearAccumulation,
1176    out: &mut [f32],
1177) {
1178    if accumulation == F32LinearAccumulation::WidenedF64 {
1179        for lane in 0..head_dim {
1180            let mut sum = 0.0f64;
1181            for (key_position, weight) in scores.iter().copied().enumerate() {
1182                let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1183                sum += f64::from(weight) * f64::from(value);
1184            }
1185            out[lane] = sum as f32;
1186        }
1187        return;
1188    }
1189    let lanes = match accumulation {
1190        F32LinearAccumulation::Scalar => 1,
1191        F32LinearAccumulation::Lanes4 | F32LinearAccumulation::FusedLanes4 => 4,
1192        F32LinearAccumulation::Lanes8 | F32LinearAccumulation::FusedLanes8 => 8,
1193        F32LinearAccumulation::Accelerate
1194        | F32LinearAccumulation::AccelerateRowInvariant
1195        | F32LinearAccumulation::AccelerateBiasSeeded
1196        | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => 1,
1197        F32LinearAccumulation::WidenedF64 => unreachable!("handled above"),
1198    };
1199    for lane in 0..head_dim {
1200        let mut partial = [0.0f32; 8];
1201        for (key_position, weight) in scores.iter().copied().enumerate() {
1202            let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1203            let partial_index = key_position % lanes;
1204            partial[partial_index] = match accumulation {
1205                F32LinearAccumulation::FusedLanes4 | F32LinearAccumulation::FusedLanes8 => {
1206                    weight.mul_add(value, partial[partial_index])
1207                }
1208                F32LinearAccumulation::Scalar
1209                | F32LinearAccumulation::Lanes4
1210                | F32LinearAccumulation::Lanes8
1211                | F32LinearAccumulation::Accelerate
1212                | F32LinearAccumulation::AccelerateRowInvariant
1213                | F32LinearAccumulation::AccelerateBiasSeeded
1214                | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
1215                | F32LinearAccumulation::WidenedF64 => partial[partial_index] + weight * value,
1216            };
1217        }
1218        let mut sum = 0.0f32;
1219        for value in &partial[..lanes] {
1220            sum += *value;
1221        }
1222        out[lane] = sum;
1223    }
1224}
1225
1226/// Collapse the three mRoPE axes into one `cos`/`sin` row using the checkpoint's INTERLEAVED rule.
1227///
1228/// The pinned config sets `rope_scaling.interleaved = true`, which selects a different branch from
1229/// the familiar section-split one — a difference that is numerically invisible whenever the three
1230/// axes carry equal positions (which OQ-4 says they always do here, all three receiving the same
1231/// scalar causal index) and therefore exactly the kind of thing a port gets wrong and only discovers
1232/// against a batched or genuinely multimodal input. It is implemented faithfully regardless.
1233///
1234/// `axes` is the first half of each axis's row, `[3][half]`; `out` receives `[half]`. Element `j`
1235/// takes axis `j % 3` while `j` lies in `1..sections[1..].max() * 3`, and axis 0 elsewhere.
1236///
1237/// # Panics
1238///
1239/// Panics if `out` is not `half` long or an axis row is short.
1240pub fn mrope_interleave(axes: [&[f32]; 3], sections: [usize; 3], out: &mut [f32]) {
1241    let half = out.len();
1242    for axis in axes {
1243        assert!(
1244            axis.len() >= half,
1245            "axis row shorter than the half-dimension"
1246        );
1247    }
1248
1249    // Start from axis 0 everywhere, then overwrite the strided lanes from axes 1 and 2, exactly as
1250    // the reference does with its `x_t[..., beg:end:3] = x[beg, ..., beg:end:3]` assignments.
1251    out.copy_from_slice(&axes[0][..half]);
1252    let modality_num = 3usize;
1253    for (axis_index, section) in sections.iter().enumerate().skip(1) {
1254        let end = section * modality_num;
1255        let mut lane = axis_index;
1256        while lane < end && lane < half {
1257            out[lane] = axes[axis_index][lane];
1258            lane += modality_num;
1259        }
1260    }
1261}
1262
1263/// Apply rotary embeddings to one head row in the `rotate_half` layout.
1264///
1265/// `row` is `[head_dim]`; `cos` and `sin` are the full `[head_dim]` rows (the doubled half). The
1266/// transform is `x*cos + rotate_half(x)*sin` where `rotate_half` maps `[a, b] -> [-b, a]` over the
1267/// two halves.
1268///
1269/// # Panics
1270///
1271/// Panics if `cos`/`sin` do not match `row`, or if `head_dim` is odd.
1272pub fn apply_rope_in_place(row: &mut [f32], cos: &[f32], sin: &[f32]) {
1273    let dim = row.len();
1274    assert_eq!(cos.len(), dim, "cos must match head_dim");
1275    assert_eq!(sin.len(), dim, "sin must match head_dim");
1276    assert!(dim.is_multiple_of(2), "head_dim must be even");
1277
1278    let half = dim / 2;
1279    let original: Vec<f32> = row.to_vec();
1280    for index in 0..dim {
1281        let rotated = if index < half {
1282            -original[index + half]
1283        } else {
1284            original[index - half]
1285        };
1286        row[index] = original[index] * cos[index] + rotated * sin[index];
1287    }
1288}
1289
1290#[cfg(test)]
1291mod tests {
1292    use super::*;
1293
1294    #[test]
1295    fn linear_matches_a_hand_computed_product() {
1296        // x = [[1, 2, 3]], weight = [[1, 0, -1], [2, 2, 2]] -> [1*1 + 2*0 + 3*-1, 2+4+6] = [-2, 12]
1297        let x = [1.0, 2.0, 3.0];
1298        let weight = [1.0, 0.0, -1.0, 2.0, 2.0, 2.0];
1299        let mut out = [0.0; 2];
1300        linear(&x, &weight, None, 1, 3, 2, &mut out);
1301        assert_eq!(out, [-2.0, 12.0]);
1302
1303        let mut biased = [0.0; 2];
1304        linear(&x, &weight, Some(&[10.0, -12.0]), 1, 3, 2, &mut biased);
1305        assert_eq!(biased, [8.0, 0.0]);
1306    }
1307
1308    #[test]
1309    fn torch_cascade_sum_agrees_with_the_flat_sum_when_rounding_cannot_intervene() {
1310        // Powers of two below 2^24 add without rounding, so every reduction order must produce the
1311        // same total. This proves the cascade's bookkeeping — its drains, its ILP chains, its tail
1312        // handling — visits each element exactly once, independently of any parity claim.
1313        for length in [1usize, 7, 8, 15, 16, 31, 128, 1024, 3072] {
1314            for width in [4usize, 8] {
1315                let values: Vec<f32> = (0..length).map(|index| (index % 8) as f32).collect();
1316                let flat: f32 = values.iter().sum();
1317                assert_eq!(
1318                    torch_cascade_sum(&values, width, |value| value),
1319                    flat,
1320                    "length {length}, width {width}"
1321                );
1322            }
1323        }
1324    }
1325
1326    #[test]
1327    fn torch_cascade_sum_applies_its_transform_before_accumulating() {
1328        let values = [1.0f32, 2.0, 3.0, 4.0, 5.0];
1329        assert_eq!(torch_cascade_sum(&values, 4, |value| value * value), 55.0);
1330    }
1331
1332    #[test]
1333    fn torch_cascade_sum_differs_from_a_flat_sum_once_rounding_matters() {
1334        // A large leading term followed by many small ones is exactly the case a cascade exists to
1335        // improve: the flat sum loses every small term into the large accumulator, the cascade does
1336        // not. If these ever agreed, the transcription would have collapsed into a flat sum and the
1337        // parity sweep would be comparing one order against itself.
1338        let mut values = vec![1.0f32; 1024];
1339        values[0] = 1.0e8;
1340        let flat = values.iter().fold(0.0f32, |sum, value| sum + value);
1341        assert_ne!(torch_cascade_sum(&values, 8, |value| value), flat);
1342    }
1343
1344    #[test]
1345    fn ceil_log2_matches_its_definition() {
1346        assert_eq!(ceil_log2(0), 0);
1347        assert_eq!(ceil_log2(1), 0);
1348        assert_eq!(ceil_log2(2), 1);
1349        assert_eq!(ceil_log2(3), 2);
1350        assert_eq!(ceil_log2(32), 5);
1351        assert_eq!(ceil_log2(33), 6);
1352    }
1353
1354    #[test]
1355    fn rms_norm_normalizes_and_scales() {
1356        // mean(x^2) for [3, 4] is 12.5; rsqrt(12.5 + 0) ~ 0.2828427
1357        let x = [3.0f32, 4.0];
1358        let weight = [1.0f32, 1.0];
1359        let mut out = [0.0; 2];
1360        rms_norm(&x, &weight, 0.0, 1, 2, &mut out);
1361        let expected = 12.5f32.sqrt().recip();
1362        assert!((out[0] - 3.0 * expected).abs() < 1e-6);
1363        assert!((out[1] - 4.0 * expected).abs() < 1e-6);
1364
1365        // The weight is applied per element, after scaling.
1366        let mut weighted = [0.0; 2];
1367        rms_norm(&x, &[2.0, 0.5], 0.0, 1, 2, &mut weighted);
1368        assert!((weighted[0] - 3.0 * expected * 2.0).abs() < 1e-6);
1369        assert!((weighted[1] - 4.0 * expected * 0.5).abs() < 1e-6);
1370    }
1371
1372    #[test]
1373    fn silu_mul_matches_the_definition() {
1374        let mut gate = [0.0f32, 1.0, -1.0];
1375        let up = [1.0f32, 2.0, 3.0];
1376        silu_mul_in_place(&mut gate, &up);
1377        assert_eq!(gate[0], 0.0);
1378        let silu_one = 1.0f32 / (1.0 + (-1.0f32).exp());
1379        assert!((gate[1] - silu_one * 2.0).abs() < 1e-6);
1380        let silu_neg = -1.0f32 / (1.0 + 1.0f32.exp());
1381        assert!((gate[2] - silu_neg * 3.0).abs() < 1e-6);
1382    }
1383
1384    #[test]
1385    fn softmax_rows_sums_to_one_and_is_shift_invariant() {
1386        let mut x = [1.0f32, 2.0, 3.0, 101.0, 102.0, 103.0];
1387        softmax_rows(&mut x, 2, 3);
1388        let first: f32 = x[..3].iter().sum();
1389        let second: f32 = x[3..].iter().sum();
1390        assert!((first - 1.0).abs() < 1e-6);
1391        assert!((second - 1.0).abs() < 1e-6);
1392        // Rows differing by a constant shift must produce identical distributions.
1393        for index in 0..3 {
1394            assert!((x[index] - x[index + 3]).abs() < 1e-6);
1395        }
1396    }
1397
1398    #[test]
1399    fn gqa_maps_each_query_head_to_its_kv_group() {
1400        let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (1, 1, 4, 2, 2);
1401        let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1402        let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1403        let values = [10.0f32, 11.0, 20.0, 21.0];
1404        let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1405
1406        gqa_attention(
1407            &queries,
1408            &keys,
1409            &values,
1410            &[0.0],
1411            query_positions,
1412            key_positions,
1413            q_heads,
1414            kv_heads,
1415            head_dim,
1416            &mut out,
1417        );
1418
1419        assert_eq!(&out[0..2], &[10.0, 11.0]);
1420        assert_eq!(&out[2..4], &[10.0, 11.0]);
1421        assert_eq!(&out[4..6], &[20.0, 21.0]);
1422        assert_eq!(&out[6..8], &[20.0, 21.0]);
1423    }
1424
1425    #[test]
1426    fn gqa_honors_the_additive_causal_mask() {
1427        let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (2, 2, 1, 1, 2);
1428        let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1429        let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1430        let values = [2.0f32, 4.0, 10.0, 20.0];
1431        let mask = [0.0f32, f32::NEG_INFINITY, 0.0, 0.0];
1432        let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1433
1434        gqa_attention(
1435            &queries,
1436            &keys,
1437            &values,
1438            &mask,
1439            query_positions,
1440            key_positions,
1441            q_heads,
1442            kv_heads,
1443            head_dim,
1444            &mut out,
1445        );
1446
1447        assert_eq!(&out[0..2], &[2.0, 4.0]);
1448        assert_eq!(&out[2..4], &[6.0, 12.0]);
1449    }
1450
1451    #[test]
1452    fn rope_rotates_a_known_pair() {
1453        // head_dim 2, cos = [0, 0], sin = [1, 1]: [a, b] -> [-b, a]
1454        let mut row = [3.0f32, 5.0];
1455        apply_rope_in_place(&mut row, &[0.0, 0.0], &[1.0, 1.0]);
1456        assert_eq!(row, [-5.0, 3.0]);
1457
1458        // Identity when cos = 1, sin = 0.
1459        let mut same = [3.0f32, 5.0];
1460        apply_rope_in_place(&mut same, &[1.0, 1.0], &[0.0, 0.0]);
1461        assert_eq!(same, [3.0, 5.0]);
1462    }
1463
1464    #[test]
1465    fn mrope_interleave_is_identity_when_all_axes_agree() {
1466        // OQ-4: all three axes carry the same scalar causal index in this model, so the interleave
1467        // must be a no-op on equal axes. If it is not, the lane arithmetic is wrong.
1468        let axis: Vec<f32> = (0..64).map(|value| value as f32).collect();
1469        let mut out = vec![0.0f32; 64];
1470        mrope_interleave([&axis, &axis, &axis], [24, 20, 20], &mut out);
1471        assert_eq!(out, axis);
1472    }
1473
1474    #[test]
1475    fn mrope_interleave_selects_the_documented_lanes() {
1476        let zeros = vec![0.0f32; 64];
1477        let ones = vec![1.0f32; 64];
1478        let twos = vec![2.0f32; 64];
1479        let mut out = vec![0.0f32; 64];
1480        mrope_interleave([&zeros, &ones, &twos], [24, 20, 20], &mut out);
1481
1482        // Lanes 1, 4, .. < 60 come from axis 1; lanes 2, 5, .. < 60 from axis 2; the rest stay 0,
1483        // including every lane at or above 60.
1484        for (lane, value) in out.iter().enumerate() {
1485            let expected = if lane < 60 && lane % 3 == 1 {
1486                1.0
1487            } else if lane < 60 && lane % 3 == 2 {
1488                2.0
1489            } else {
1490                0.0
1491            };
1492            assert_eq!(*value, expected, "lane {lane}");
1493        }
1494    }
1495}