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    // The BLAS-less fall-through: no platform GEMM was available (or none was asked for).
212    //
213    // Every dense op in the codec — both convolutions via im2col, the ConvNeXt pointwise pair, the
214    // transformer's q/k/v/o and FFN — reaches this one function, so the kernel chosen here is the
215    // codec's entire arithmetic budget. In the browser that budget measured 89.1 s of a 97.3 s
216    // frame (92%), because a per-element dot product re-reads the activation row once per output
217    // column and reuses no weight at all.
218    //
219    // `linear_packed` is the register-tiled, panel-packed replacement, and it is safe to
220    // substitute HERE specifically because of what it does not change: each output element still
221    // accumulates over ascending `k` into its own slot, one add at a time, so it is bit-identical
222    // to the `Scalar` reduction (`packed_matches_scalar_bit_for_bit`).
223    //
224    // Two regimes, and only one of them changes any bits:
225    //
226    //   * native non-macOS — a denied BLAS request already degrades to lanes = 1, i.e. the scalar
227    //     order. Packed reproduces it exactly, so this is a pure speed change with NO numerics
228    //     change and nothing to ledger.
229    //   * wasm — the fall-through used eight independent partial chains (a non-reference order,
230    //     adopted because a single scalar chain cannot be autovectorized). Packed replaces that
231    //     with the reference's own order, so the browser moves CLOSER to the oracle while getting
232    //     faster. A speed lever that tightens parity rather than loosening it.
233    //
234    // `m == 1` keeps the dot path: with one row there is nothing to amortize a packed panel over,
235    // and NE-004 measured register blocking as neutral at that geometry.
236    //
237    // The gate below also keeps the forensic probe orders OFF this path. `Lanes4/8`,
238    // `FusedLanes4/8`, and `WidenedF64` exist solely so the parity harness can reproduce a
239    // specific non-scalar reduction order; routing them through packed/team would silently
240    // hand every probe the Scalar order and turn the attribution sweep into Scalar-vs-Scalar
241    // under five labels. Only the orders packed provably reproduces may take the shortcut:
242    // `Scalar` itself, and the denied-BLAS `Accelerate*` degradations documented above.
243    let packed_preserves_order = matches!(
244        accumulation,
245        F32LinearAccumulation::Scalar
246            | F32LinearAccumulation::Accelerate
247            | F32LinearAccumulation::AccelerateRowInvariant
248            | F32LinearAccumulation::AccelerateBiasSeeded
249            | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
250    );
251    if m > 1 && packed_preserves_order {
252        // Hand it to the team when one is armed. This is the codec's entire arithmetic budget —
253        // 92% of browser frame time — and it ran on one thread while every worker sat parked.
254        //
255        // Partitioning is a pure speed knob: stripes are disjoint columns, no reduction is split,
256        // so the parallel result is bit-identical per element to the serial packed kernel. The
257        // work must also be big enough to pay for a dispatch, hence the floor; and a worker thread
258        // that is itself inside a kernel must never re-dispatch (`thread_bypassed`).
259        const TEAM_FLOOR: usize = 64 * 1024;
260        if m * n >= TEAM_FLOOR
261            && !crate::team::thread_bypassed()
262            && let Some(team) = crate::team::armed()
263        {
264            team.linear_f32(x, weight, bias, m, k, n, out);
265            return;
266        }
267        crate::packed_gemm::linear_packed(x, weight, bias, m, k, n, out);
268        return;
269    }
270
271    for row in 0..m {
272        let x_row = &x[row * k..row * k + k];
273        for col in 0..n {
274            let w_row = &weight[col * k..col * k + k];
275            let sum = dot_with_accumulation(x_row, w_row, accumulation);
276            out[row * n + col] = bias.map_or(sum, |b| sum + b[col]);
277        }
278    }
279}
280
281fn dot_with_accumulation(x: &[f32], weight: &[f32], accumulation: F32LinearAccumulation) -> f32 {
282    assert_eq!(x.len(), weight.len(), "dot-product inputs must match");
283    match accumulation {
284        F32LinearAccumulation::Scalar => {
285            let mut sum = 0.0f32;
286            for index in 0..x.len() {
287                sum += x[index] * weight[index];
288            }
289            sum
290        }
291        F32LinearAccumulation::WidenedF64 => {
292            let mut sum = 0.0f64;
293            for index in 0..x.len() {
294                sum += f64::from(x[index]) * f64::from(weight[index]);
295            }
296            sum as f32
297        }
298        F32LinearAccumulation::Lanes4
299        | F32LinearAccumulation::Lanes8
300        | F32LinearAccumulation::FusedLanes4
301        | F32LinearAccumulation::FusedLanes8
302        | F32LinearAccumulation::Accelerate
303        | F32LinearAccumulation::AccelerateRowInvariant
304        | F32LinearAccumulation::AccelerateBiasSeeded
305        | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => {
306            let lanes = match accumulation {
307                F32LinearAccumulation::Lanes4 => 4,
308                F32LinearAccumulation::Lanes8 => 8,
309                F32LinearAccumulation::FusedLanes4 => 4,
310                F32LinearAccumulation::FusedLanes8 => 8,
311                F32LinearAccumulation::Accelerate
312                | F32LinearAccumulation::AccelerateRowInvariant
313                | F32LinearAccumulation::AccelerateBiasSeeded
314                | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => {
315                    // A denied BLAS request degrades to lanes = 1 (the scalar order) on native
316                    // targets — pinned behavior for Linux parity. On wasm32 that scalar f32
317                    // reduction chain cannot be autovectorized (f32 addition is not
318                    // reassociable) and profiled as 71% of ALL synthesis time; eight partial
319                    // chains give the compiler independent accumulators. Both orders live in
320                    // the same "correct, not exact" contract the denied-BLAS path already
321                    // declares.
322                    #[cfg(target_arch = "wasm32")]
323                    {
324                        8
325                    }
326                    #[cfg(not(target_arch = "wasm32"))]
327                    {
328                        1
329                    }
330                }
331                F32LinearAccumulation::Scalar | F32LinearAccumulation::WidenedF64 => {
332                    unreachable!("scalar and widened orders are handled above")
333                }
334            };
335            let mut partial = [0.0f32; 8];
336            for index in 0..x.len() {
337                let lane = index % lanes;
338                partial[lane] = match accumulation {
339                    F32LinearAccumulation::FusedLanes4 | F32LinearAccumulation::FusedLanes8 => {
340                        x[index].mul_add(weight[index], partial[lane])
341                    }
342                    F32LinearAccumulation::Scalar
343                    | F32LinearAccumulation::Lanes4
344                    | F32LinearAccumulation::Lanes8
345                    | F32LinearAccumulation::Accelerate
346                    | F32LinearAccumulation::AccelerateRowInvariant
347                    | F32LinearAccumulation::AccelerateBiasSeeded
348                    | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
349                    | F32LinearAccumulation::WidenedF64 => partial[lane] + x[index] * weight[index],
350                };
351            }
352            let mut sum = 0.0f32;
353            for value in &partial[..lanes] {
354                sum += *value;
355            }
356            sum
357        }
358    }
359}
360
361/// Attempts the same row-major SGEMM backend recorded by the pinned macOS CPU-fp32 oracle.
362///
363/// The test-only [`F32LinearAccumulation::Accelerate`] selector keeps this foreign call out of the
364/// normal safe-Rust reference path. Targets without the opt-in macOS backend return `false`, so
365/// their scalar fallback remains bit-for-bit the ordinary reference implementation.
366#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
367#[allow(clippy::too_many_arguments)]
368fn accelerate_sgemm(
369    x: &[f32],
370    weight: &[f32],
371    m: usize,
372    k: usize,
373    n: usize,
374    beta: f32,
375    row_invariant: bool,
376    out: &mut [f32],
377) -> bool {
378    // Accelerate routes M = 1 to a GEMV kernel whose reduction order differs from its M >= 2
379    // GEMM kernel in the last ulps, while M >= 2 is row-invariant (measured: M of 2, 3, 4 and 14
380    // produce bit-identical rows). A streaming decode presents the same convolution at M = packet
381    // while offline presents M = utterance, so without this pinning the streaming == offline gate
382    // fails on every 1-frame packet. Under `row_invariant`, present M = 1 as a duplicated-row
383    // M = 2 call and keep row 0, so every M reduces on the same kernel path. Seams the ORACLE
384    // itself computes at M = 1 (the speaker-encoder embedding head) must NOT request this:
385    // the GEMV bits ARE the oracle's bits there.
386    if row_invariant && m == 1 {
387        let mut doubled_x = Vec::with_capacity(2 * k);
388        doubled_x.extend_from_slice(x);
389        doubled_x.extend_from_slice(x);
390        let mut doubled_out = Vec::with_capacity(2 * n);
391        doubled_out.extend_from_slice(out);
392        doubled_out.extend_from_slice(out);
393        if !accelerate_sgemm(&doubled_x, weight, 2, k, n, beta, false, &mut doubled_out) {
394            return false;
395        }
396        out.copy_from_slice(&doubled_out[..n]);
397        return true;
398    }
399    let m = i32::try_from(m).expect("SGEMM rows fit CBLAS i32 dimensions");
400    let k = i32::try_from(k).expect("SGEMM reduction fits CBLAS i32 dimensions");
401    let n = i32::try_from(n).expect("SGEMM columns fit CBLAS i32 dimensions");
402    // SAFETY: `linear_with_accumulation` proves the row-major slice lengths before this call.
403    // `x` is M×K, `weight` is N×K and is passed transposed, and `out` is M×N. All pointers remain
404    // valid and non-overlapping for the full synchronous CBLAS call.
405    unsafe {
406        cblas_sgemm(
407            CBLAS_ROW_MAJOR,
408            CBLAS_NO_TRANSPOSE,
409            CBLAS_TRANSPOSE,
410            m,
411            n,
412            k,
413            1.0,
414            x.as_ptr(),
415            k,
416            weight.as_ptr(),
417            k,
418            beta,
419            out.as_mut_ptr(),
420            n,
421        );
422    }
423    true
424}
425
426#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
427fn accelerate_sgemm(
428    _x: &[f32],
429    _weight: &[f32],
430    _m: usize,
431    _k: usize,
432    _n: usize,
433    _beta: f32,
434    _row_invariant: bool,
435    _out: &mut [f32],
436) -> bool {
437    false
438}
439
440#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
441const CBLAS_ROW_MAJOR: i32 = 101;
442#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
443const CBLAS_NO_TRANSPOSE: i32 = 111;
444#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
445const CBLAS_TRANSPOSE: i32 = 112;
446
447#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
448#[link(name = "Accelerate", kind = "framework")]
449unsafe extern "C" {
450    fn cblas_sgemm(
451        order: i32,
452        trans_a: i32,
453        trans_b: i32,
454        m: i32,
455        n: i32,
456        k: i32,
457        alpha: f32,
458        a: *const f32,
459        lda: i32,
460        b: *const f32,
461        ldb: i32,
462        beta: f32,
463        c: *mut f32,
464        ldc: i32,
465    );
466}
467
468/// Which `sin`/`exp` implementation an elementwise parity probe evaluates.
469///
470/// The reference stack does not call the scalar libm for large tensors: its CPU elementwise kernels
471/// dispatch through a vectorized `Vectorized<float>`, whose transcendentals are ~1-ulp routines
472/// rather than correctly-rounded ones. `codec_snake_bisect` established that the SnakeBeta seam's
473/// entire residual divergence lives in exactly these two functions — every other operation in that
474/// expression is a correctly-rounded f32 `*`, `+` or `/` with no freedom at all — so identifying
475/// *which* vectorized routine the pinned oracle used is the whole remaining question there.
476#[derive(Clone, Copy, Debug, Eq, PartialEq)]
477pub enum F32Transcendental {
478    /// Rust's scalar `f32::sin` / `f32::exp`, i.e. the platform libm. The production reference.
479    ScalarLibm,
480    /// macOS Accelerate vForce (`vvsinf` / `vvexpf`), selected only by the parity harness.
481    ///
482    /// On every other target this deliberately falls back to [`Self::ScalarLibm`].
483    AccelerateVForce,
484    /// SLEEF's 1-ulp `Sleef_sinf_u10` / `Sleef_expf_u10`, ported to safe Rust in [`crate::sleef`].
485    ///
486    /// This is the routine an AArch64 `Vectorized<float>` actually dispatches to, so it is the
487    /// candidate the vForce probe was only ever standing in for — and unlike vForce it is portable
488    /// and could therefore be adopted into production if it measures exact.
489    SleefU10,
490}
491
492/// Fills `out` with `sin(x)` under the selected implementation.
493///
494/// # Panics
495///
496/// Panics if `out` is not the same length as `x`.
497pub fn sin_with(x: &[f32], implementation: F32Transcendental, out: &mut [f32]) {
498    assert_eq!(x.len(), out.len(), "sin output must match its input");
499    if implementation == F32Transcendental::AccelerateVForce && vforce_sin(x, out) {
500        return;
501    }
502    if implementation == F32Transcendental::SleefU10 {
503        for (value, target) in x.iter().zip(out.iter_mut()) {
504            *target = crate::sleef::sinf_u10(*value);
505        }
506        return;
507    }
508    for (value, target) in x.iter().zip(out.iter_mut()) {
509        *target = value.sin();
510    }
511}
512
513/// Fills `out` with `exp(x)` under the selected implementation.
514///
515/// # Panics
516///
517/// Panics if `out` is not the same length as `x`.
518pub fn exp_with(x: &[f32], implementation: F32Transcendental, out: &mut [f32]) {
519    assert_eq!(x.len(), out.len(), "exp output must match its input");
520    if implementation == F32Transcendental::AccelerateVForce && vforce_exp(x, out) {
521        return;
522    }
523    if implementation == F32Transcendental::SleefU10 {
524        for (value, target) in x.iter().zip(out.iter_mut()) {
525            *target = crate::sleef::expf_u10(*value);
526        }
527        return;
528    }
529    for (value, target) in x.iter().zip(out.iter_mut()) {
530        *target = value.exp();
531    }
532}
533
534#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
535fn vforce_sin(x: &[f32], out: &mut [f32]) -> bool {
536    let count = i32::try_from(x.len()).expect("vForce length fits i32");
537    // SAFETY: `sin_with` proved the two slices have equal length, and they are distinct live
538    // allocations for the duration of this synchronous call.
539    unsafe { vvsinf(out.as_mut_ptr(), x.as_ptr(), &raw const count) };
540    true
541}
542
543#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
544fn vforce_exp(x: &[f32], out: &mut [f32]) -> bool {
545    let count = i32::try_from(x.len()).expect("vForce length fits i32");
546    // SAFETY: as in `vforce_sin`.
547    unsafe { vvexpf(out.as_mut_ptr(), x.as_ptr(), &raw const count) };
548    true
549}
550
551#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
552fn vforce_sin(_x: &[f32], _out: &mut [f32]) -> bool {
553    false
554}
555
556#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
557fn vforce_exp(_x: &[f32], _out: &mut [f32]) -> bool {
558    false
559}
560
561#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
562#[link(name = "Accelerate", kind = "framework")]
563unsafe extern "C" {
564    fn vvsinf(out: *mut f32, x: *const f32, count: *const i32);
565    fn vvexpf(out: *mut f32, x: *const f32, count: *const i32);
566}
567
568/// Qwen3 RMSNorm: `x * rsqrt(mean(x^2) + eps) * weight`, weight-only, no centering.
569///
570/// # Panics
571///
572/// Panics if `x` is not `rows * dim` elements or `weight` is not `dim`.
573pub fn rms_norm(x: &[f32], weight: &[f32], eps: f32, rows: usize, dim: usize, out: &mut [f32]) {
574    rms_norm_with_arithmetic(
575        x,
576        weight,
577        eps,
578        rows,
579        dim,
580        F32RmsNormArithmetic::ScalarReciprocalSqrt,
581        out,
582    );
583}
584
585/// Same operation as [`rms_norm`], with an explicitly selected reduction and scale calculation.
586///
587/// This entry point is for CPU-fp32 parity forensics; [`rms_norm`] remains the normal scalar f32
588/// reference path.
589pub fn rms_norm_with_arithmetic(
590    x: &[f32],
591    weight: &[f32],
592    eps: f32,
593    rows: usize,
594    dim: usize,
595    arithmetic: F32RmsNormArithmetic,
596    out: &mut [f32],
597) {
598    assert_eq!(x.len(), rows * dim, "x must be [rows, dim]");
599    assert_eq!(weight.len(), dim, "weight must be [dim]");
600    assert_eq!(out.len(), rows * dim, "out must be [rows, dim]");
601
602    for row in 0..rows {
603        let src = &x[row * dim..row * dim + dim];
604        let scale = rms_scale(src, eps, arithmetic);
605        for index in 0..dim {
606            out[row * dim + index] = src[index] * scale * weight[index];
607        }
608    }
609}
610
611fn rms_scale(src: &[f32], eps: f32, arithmetic: F32RmsNormArithmetic) -> f32 {
612    match arithmetic {
613        F32RmsNormArithmetic::ScalarReciprocalSqrt => {
614            let sum = sum_squares_f32(src, 1);
615            (sum / src.len() as f32 + eps).sqrt().recip()
616        }
617        F32RmsNormArithmetic::ScalarDivideSqrt => {
618            let sum = sum_squares_f32(src, 1);
619            1.0f32 / (sum / src.len() as f32 + eps).sqrt()
620        }
621        F32RmsNormArithmetic::Lanes4ReciprocalSqrt => {
622            let sum = sum_squares_f32(src, 4);
623            (sum / src.len() as f32 + eps).sqrt().recip()
624        }
625        F32RmsNormArithmetic::Lanes8ReciprocalSqrt => {
626            let sum = sum_squares_f32(src, 8);
627            (sum / src.len() as f32 + eps).sqrt().recip()
628        }
629        F32RmsNormArithmetic::Lanes16ReciprocalSqrt => {
630            let sum = sum_squares_f32(src, 16);
631            (sum / src.len() as f32 + eps).sqrt().recip()
632        }
633        F32RmsNormArithmetic::Lanes32ReciprocalSqrt => {
634            let sum = sum_squares_f32(src, 32);
635            (sum / src.len() as f32 + eps).sqrt().recip()
636        }
637        F32RmsNormArithmetic::TorchCascade4ReciprocalSqrt => {
638            let sum = torch_cascade_sum(src, 4, |value| value * value);
639            (sum / src.len() as f32 + eps).sqrt().recip()
640        }
641        F32RmsNormArithmetic::TorchCascade8ReciprocalSqrt => {
642            let sum = torch_cascade_sum(src, 8, |value| value * value);
643            (sum / src.len() as f32 + eps).sqrt().recip()
644        }
645        F32RmsNormArithmetic::F64ReciprocalSqrt => {
646            let mut sum = 0.0f64;
647            for value in src {
648                let value = f64::from(*value);
649                sum += value * value;
650            }
651            (sum / src.len() as f64 + f64::from(eps)).sqrt().recip() as f32
652        }
653    }
654}
655
656fn sum_squares_f32(src: &[f32], lanes: usize) -> f32 {
657    let mut partial = [0.0f32; 32];
658    for (index, value) in src.iter().enumerate() {
659        partial[index % lanes] += *value * *value;
660    }
661    let mut sum = 0.0f32;
662    for value in &partial[..lanes] {
663        sum += *value;
664    }
665    sum
666}
667
668/// The reference stack's contiguous-inner-dimension f32 sum, transcribed operation for operation.
669///
670/// Every other reduction offered here is a *guess* at the oracle's order — "four accumulators
671/// landed closer, so perhaps it used four". This one is not a guess: it is the shape PyTorch's
672/// `SumKernel.cpp` actually reduces with, and it is materially different from any flat lane
673/// interleave, so a flat lane order that lands close can still never land on it.
674///
675/// Three nested structures, outermost first:
676///
677/// 1. **Vector lanes.** The row is walked `width` elements at a time and each lane keeps its own
678///    running sum. `width` is `Vectorized<float>::size()`, 8 on an ARM build with
679///    `AT_BUILD_ARM_VEC256_WITH_SLEEF` and 4 without, which is why it is a parameter here rather
680///    than a constant: it is the one part of the shape the provenance does not pin.
681/// 2. **Instruction-level parallelism.** `row_sum` splits the vector stream into `ILP = 4`
682///    independent chains, reduced `p0 += p1; p0 += p2; p0 += p3` at the end.
683/// 3. **Cascade.** Each chain is not a flat running sum but a `LEVELS = 4` deep cascade: level 0
684///    absorbs `level_step` vectors and is then drained into level 1, level 1 into level 2 when its
685///    own counter wraps, and so on. This is what bounds the reduction's error growth, and it makes
686///    the partial-sum magnitudes — and therefore the rounding — differ from a flat sum even at
687///    identical lane counts.
688///
689/// The final horizontal fold is left-to-right over the lanes, as `vectorized_inner_sum` does when
690/// it stores the accumulator and sums the array.
691///
692/// `transform` is applied to each element before accumulation, so RMSNorm's `pow(2).mean(-1)` can
693/// square in the same pass the reference's separate `pow(2)` tensor would have.
694///
695/// # Panics
696///
697/// Panics if `width` is zero.
698// Index loops keep the reference's exact accumulation order (level, chain, lane) visible;
699// iterator rewrites would obscure the summation-order argument this port is documenting.
700#[allow(clippy::needless_range_loop)]
701pub fn torch_cascade_sum(src: &[f32], width: usize, transform: impl Fn(f32) -> f32) -> f32 {
702    assert!(width > 0, "vector width must be positive");
703    const ILP: usize = 4;
704    const LEVELS: usize = 4;
705
706    let vector_count = src.len() / width;
707    let vector = |index: usize, lane: usize| transform(src[index * width + lane]);
708
709    // `multi_row_sum(in_data, row_stride = col_stride * ILP, col_stride, size = vector_count / ILP)`
710    let size = vector_count / ILP;
711    let level_power = ceil_log2(size).div_euclid(LEVELS).max(4);
712    let level_step = 1usize << level_power;
713    let level_mask = level_step - 1;
714
715    let mut acc = vec![[0.0f32; ILP].map(|_| vec![0.0f32; width]); LEVELS];
716    let mut index = 0usize;
717    while index + level_step <= size {
718        for _ in 0..level_step {
719            for chain in 0..ILP {
720                for lane in 0..width {
721                    acc[0][chain][lane] += vector(index * ILP + chain, lane);
722                }
723            }
724            index += 1;
725        }
726        for level in 1..LEVELS {
727            for chain in 0..ILP {
728                for lane in 0..width {
729                    acc[level][chain][lane] += acc[level - 1][chain][lane];
730                    acc[level - 1][chain][lane] = 0.0;
731                }
732            }
733            if index & (level_mask << (level * level_power)) != 0 {
734                break;
735            }
736        }
737    }
738    while index < size {
739        for chain in 0..ILP {
740            for lane in 0..width {
741                acc[0][chain][lane] += vector(index * ILP + chain, lane);
742            }
743        }
744        index += 1;
745    }
746    for level in 1..LEVELS {
747        for chain in 0..ILP {
748            for lane in 0..width {
749                acc[level][chain][lane] += acc[level - 1][chain][lane];
750            }
751        }
752    }
753
754    // `row_sum`: absorb the vectors `multi_row_sum` could not group, then fold the ILP chains.
755    let mut partial = acc.swap_remove(LEVELS - 1);
756    for leftover in size * ILP..vector_count {
757        for lane in 0..width {
758            partial[0][lane] += vector(leftover, lane);
759        }
760    }
761    for chain in 1..ILP {
762        for lane in 0..width {
763            partial[0][lane] += partial[chain][lane];
764        }
765    }
766
767    // `vectorized_inner_sum`: the elements past the last whole vector are summed first, then the
768    // lanes are folded into that running total left to right.
769    let mut sum = 0.0f32;
770    for index in vector_count * width..src.len() {
771        sum += transform(src[index]);
772    }
773    for lane in 0..width {
774        sum += partial[0][lane];
775    }
776    sum
777}
778
779/// `c10::llvm::CeilLog2` for the sizes this reduction sees: `ceil(log2(value))`, zero for `0` and `1`.
780fn ceil_log2(value: usize) -> usize {
781    if value <= 1 {
782        return 0;
783    }
784    usize::BITS as usize - (value - 1).leading_zeros() as usize
785}
786
787/// SwiGLU's elementwise half: `silu(gate) * up`, written into `gate`.
788pub fn silu_mul_in_place(gate: &mut [f32], up: &[f32]) {
789    silu_mul_in_place_with_arithmetic(gate, up, F32SiluArithmetic::Divide);
790}
791
792/// Same operation as [`silu_mul_in_place`], with an explicitly chosen f32 association.
793pub fn silu_mul_in_place_with_arithmetic(
794    gate: &mut [f32],
795    up: &[f32],
796    arithmetic: F32SiluArithmetic,
797) {
798    assert_eq!(gate.len(), up.len(), "gate and up must match");
799    for (g, u) in gate.iter_mut().zip(up) {
800        let x = *g;
801        if arithmetic == F32SiluArithmetic::WidenedF64 {
802            let wide = f64::from(x);
803            *g = (wide / (1.0 + (-wide).exp()) * f64::from(*u)) as f32;
804            continue;
805        }
806        let denominator = 1.0 + (-x).exp();
807        let silu = match arithmetic {
808            F32SiluArithmetic::Divide => x / denominator,
809            F32SiluArithmetic::MultiplyReciprocal => x * denominator.recip(),
810            F32SiluArithmetic::WidenedF64 => unreachable!("handled above"),
811        };
812        *g = silu * u;
813    }
814}
815
816/// In-place row-wise softmax in f32, max-subtracted for stability.
817pub fn softmax_rows(x: &mut [f32], rows: usize, cols: usize) {
818    softmax_rows_with_arithmetic(x, rows, cols, F32SoftmaxArithmetic::ReciprocalMultiply);
819}
820
821/// Same operation as [`softmax_rows`], with an explicitly selected normalization form.
822pub fn softmax_rows_with_arithmetic(
823    x: &mut [f32],
824    rows: usize,
825    cols: usize,
826    arithmetic: F32SoftmaxArithmetic,
827) {
828    assert_eq!(x.len(), rows * cols, "x must be [rows, cols]");
829    for row in 0..rows {
830        let slice = &mut x[row * cols..row * cols + cols];
831        let mut max = f32::NEG_INFINITY;
832        for value in slice.iter() {
833            if *value > max {
834                max = *value;
835            }
836        }
837        if arithmetic == F32SoftmaxArithmetic::WidenedF64 {
838            let max = f64::from(max);
839            let mut wide = Vec::with_capacity(slice.len());
840            let mut sum = 0.0f64;
841            for value in slice.iter() {
842                let exponent = (f64::from(*value) - max).exp();
843                sum += exponent;
844                wide.push(exponent);
845            }
846            for (value, exponent) in slice.iter_mut().zip(wide) {
847                *value = (exponent / sum) as f32;
848            }
849            continue;
850        }
851        let mut sum = 0.0f32;
852        for value in slice.iter_mut() {
853            *value = (*value - max).exp();
854            sum += *value;
855        }
856        for value in slice.iter_mut() {
857            *value = match arithmetic {
858                F32SoftmaxArithmetic::ReciprocalMultiply => *value * sum.recip(),
859                F32SoftmaxArithmetic::Divide => *value / sum,
860                F32SoftmaxArithmetic::WidenedF64 => unreachable!("handled above"),
861            };
862        }
863    }
864}
865
866/// Grouped-query attention for row-major f32 tensors.
867///
868/// `queries` and `out` are `[query_positions, q_heads, head_dim]`; `keys` and `values` are
869/// `[key_positions, kv_heads, head_dim]`; `additive_mask` is `[query_positions, key_positions]`.
870/// Query head `h` reads key/value head `h / (q_heads / kv_heads)`, matching Qwen3-TTS's 16 query
871/// heads over 8 KV heads. The reduction order is scalar and fixed so an ISA-specific kernel has a
872/// direct f32 reference to compare against.
873///
874/// # Panics
875///
876/// Panics if the dimensions disagree or query heads are not evenly grouped over KV heads.
877#[allow(clippy::too_many_arguments)]
878pub fn gqa_attention(
879    queries: &[f32],
880    keys: &[f32],
881    values: &[f32],
882    additive_mask: &[f32],
883    query_positions: usize,
884    key_positions: usize,
885    q_heads: usize,
886    kv_heads: usize,
887    head_dim: usize,
888    out: &mut [f32],
889) {
890    gqa_attention_with_softmax(
891        queries,
892        keys,
893        values,
894        additive_mask,
895        query_positions,
896        key_positions,
897        q_heads,
898        kv_heads,
899        head_dim,
900        F32SoftmaxArithmetic::ReciprocalMultiply,
901        out,
902    );
903}
904
905/// Same operation as [`gqa_attention`], with an explicitly selected softmax normalization form.
906#[allow(clippy::too_many_arguments)]
907pub fn gqa_attention_with_softmax(
908    queries: &[f32],
909    keys: &[f32],
910    values: &[f32],
911    additive_mask: &[f32],
912    query_positions: usize,
913    key_positions: usize,
914    q_heads: usize,
915    kv_heads: usize,
916    head_dim: usize,
917    softmax_arithmetic: F32SoftmaxArithmetic,
918    out: &mut [f32],
919) {
920    gqa_attention_with_arithmetic(
921        queries,
922        keys,
923        values,
924        additive_mask,
925        query_positions,
926        key_positions,
927        q_heads,
928        kv_heads,
929        head_dim,
930        softmax_arithmetic,
931        F32LinearAccumulation::Scalar,
932        out,
933    );
934}
935
936/// Same operation as [`gqa_attention`], with selected softmax and dot-product reduction forms.
937#[allow(clippy::too_many_arguments)]
938pub fn gqa_attention_with_arithmetic(
939    queries: &[f32],
940    keys: &[f32],
941    values: &[f32],
942    additive_mask: &[f32],
943    query_positions: usize,
944    key_positions: usize,
945    q_heads: usize,
946    kv_heads: usize,
947    head_dim: usize,
948    softmax_arithmetic: F32SoftmaxArithmetic,
949    accumulation: F32LinearAccumulation,
950    out: &mut [f32],
951) {
952    assert!(kv_heads > 0, "at least one KV head is required");
953    assert_eq!(
954        q_heads % kv_heads,
955        0,
956        "query heads must divide evenly into KV groups"
957    );
958    assert_eq!(
959        queries.len(),
960        query_positions * q_heads * head_dim,
961        "queries must be [query_positions, q_heads, head_dim]"
962    );
963    assert_eq!(
964        keys.len(),
965        key_positions * kv_heads * head_dim,
966        "keys must be [key_positions, kv_heads, head_dim]"
967    );
968    assert_eq!(
969        values.len(),
970        key_positions * kv_heads * head_dim,
971        "values must be [key_positions, kv_heads, head_dim]"
972    );
973    assert_eq!(
974        additive_mask.len(),
975        query_positions * key_positions,
976        "mask must be [query_positions, key_positions]"
977    );
978    assert_eq!(
979        out.len(),
980        query_positions * q_heads * head_dim,
981        "out must be [query_positions, q_heads, head_dim]"
982    );
983
984    if accumulation == F32LinearAccumulation::Accelerate
985        && accelerate_gqa_attention(
986            queries,
987            keys,
988            values,
989            additive_mask,
990            query_positions,
991            key_positions,
992            q_heads,
993            kv_heads,
994            head_dim,
995            softmax_arithmetic,
996            out,
997        )
998    {
999        return;
1000    }
1001
1002    gqa_attention_head_range_with_arithmetic(
1003        queries,
1004        keys,
1005        values,
1006        additive_mask,
1007        query_positions,
1008        key_positions,
1009        q_heads,
1010        kv_heads,
1011        head_dim,
1012        softmax_arithmetic,
1013        accumulation,
1014        0..q_heads,
1015        out,
1016    );
1017}
1018
1019/// The scalar GQA loop restricted to `q_head_range`, writing only those heads' output spans.
1020///
1021/// This is the SAME loop [`gqa_attention_with_arithmetic`] runs — extracted, not duplicated —
1022/// so a partitioned caller (the worker team) composes the identical arithmetic per head and the
1023/// full-range serial call remains the reference. Heads are independent: no reduction crosses a
1024/// head, which is why partitioning here is bit-exact rather than merely close.
1025///
1026/// # Panics
1027///
1028/// Panics if the range exceeds `q_heads`. Full shape validation is the full-range caller's job;
1029/// partitioned callers must have validated once before splitting.
1030#[allow(clippy::too_many_arguments)]
1031pub fn gqa_attention_head_range_with_arithmetic(
1032    queries: &[f32],
1033    keys: &[f32],
1034    values: &[f32],
1035    additive_mask: &[f32],
1036    query_positions: usize,
1037    key_positions: usize,
1038    q_heads: usize,
1039    kv_heads: usize,
1040    head_dim: usize,
1041    softmax_arithmetic: F32SoftmaxArithmetic,
1042    accumulation: F32LinearAccumulation,
1043    q_head_range: std::ops::Range<usize>,
1044    out: &mut [f32],
1045) {
1046    assert!(
1047        out.len() >= query_positions * q_heads * head_dim,
1048        "attention output must hold [query_positions, q_heads, head_dim]"
1049    );
1050    let out = out.as_mut_ptr();
1051    // SAFETY: the pointer comes from the `&mut` slice above, whose length was just checked to
1052    // cover every index the head range can reach, and it is not used after this call.
1053    unsafe {
1054        gqa_attention_head_range_into(
1055            queries,
1056            keys,
1057            values,
1058            additive_mask,
1059            query_positions,
1060            key_positions,
1061            q_heads,
1062            kv_heads,
1063            head_dim,
1064            softmax_arithmetic,
1065            accumulation,
1066            q_head_range,
1067            out,
1068        );
1069    }
1070}
1071
1072/// The head-range attention loop, writing through a raw output pointer.
1073///
1074/// This exists so parallel workers never have to materialize a `&mut [f32]` over the whole output
1075/// while a sibling worker holds one too. Disjoint *writes* are not enough for that to be sound:
1076/// two live `&mut` into the same allocation is undefined behaviour whatever the access pattern,
1077/// and `rustc` marks `&mut` parameters `noalias`, so it is the optimizer — not just the model —
1078/// that the overlap would mislead. Here each worker turns the pointer into a `&mut` covering
1079/// exactly the one head span it is about to write, and those spans are disjoint by construction.
1080///
1081/// # Safety
1082///
1083/// `out` must be valid for writes across `[query_positions, q_heads, head_dim]`, and no other
1084/// reference may alias the `head_dim` spans this call's `q_head_range` writes for its duration.
1085// SAFETY: discharged by both callers — the safe wrapper asserts `out.len()` and holds the only
1086// `&mut`; the team gives each worker a disjoint `q_head_range` whose `head_dim` spans cannot
1087// overlap, and blocks until every partition reports done, so `out` outlives all writes.
1088#[allow(clippy::too_many_arguments)]
1089pub(crate) unsafe fn gqa_attention_head_range_into(
1090    queries: &[f32],
1091    keys: &[f32],
1092    values: &[f32],
1093    additive_mask: &[f32],
1094    query_positions: usize,
1095    key_positions: usize,
1096    q_heads: usize,
1097    kv_heads: usize,
1098    head_dim: usize,
1099    softmax_arithmetic: F32SoftmaxArithmetic,
1100    accumulation: F32LinearAccumulation,
1101    q_head_range: std::ops::Range<usize>,
1102    out: *mut f32,
1103) {
1104    assert!(q_head_range.end <= q_heads, "head range exceeds q_heads");
1105    let scale = (head_dim as f32).sqrt().recip();
1106    let kv_group = q_heads / kv_heads;
1107    let mut scores = vec![0.0f32; key_positions];
1108
1109    for query_position in 0..query_positions {
1110        let mask =
1111            &additive_mask[query_position * key_positions..(query_position + 1) * key_positions];
1112        for q_head in q_head_range.clone() {
1113            let kv_head = q_head / kv_group;
1114            let query_base = (query_position * q_heads + q_head) * head_dim;
1115            let query = &queries[query_base..query_base + head_dim];
1116            for (key_position, score) in scores.iter_mut().enumerate() {
1117                let key_base = (key_position * kv_heads + kv_head) * head_dim;
1118                let key = &keys[key_base..key_base + head_dim];
1119                let dot = dot_with_accumulation(query, key, accumulation);
1120                *score = dot * scale + mask[key_position];
1121            }
1122            softmax_rows_with_arithmetic(&mut scores, 1, key_positions, softmax_arithmetic);
1123
1124            // SAFETY: `query_base` indexes [query_position, q_head, head_dim] inside the bounds
1125            // the caller guaranteed, and this borrow spans only this head — the one span this
1126            // partition owns, disjoint from every other partition's.
1127            let head_out = unsafe { std::slice::from_raw_parts_mut(out.add(query_base), head_dim) };
1128            attention_weighted_sum(
1129                &scores,
1130                values,
1131                kv_head,
1132                kv_heads,
1133                head_dim,
1134                accumulation,
1135                head_out,
1136            );
1137        }
1138    }
1139}
1140
1141/// Executes the two attention matrix products through the exact macOS SGEMM candidate.
1142///
1143/// This is intentionally an L2-parity probe rather than the normal attention route. The gather
1144/// buffers present the strided GQA heads as the row-major matrices consumed by CBLAS; the scalar
1145/// path above remains the cross-platform reference and the fallback when this candidate is off.
1146#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
1147#[allow(clippy::too_many_arguments)]
1148fn accelerate_gqa_attention(
1149    queries: &[f32],
1150    keys: &[f32],
1151    values: &[f32],
1152    additive_mask: &[f32],
1153    query_positions: usize,
1154    key_positions: usize,
1155    q_heads: usize,
1156    kv_heads: usize,
1157    head_dim: usize,
1158    softmax_arithmetic: F32SoftmaxArithmetic,
1159    out: &mut [f32],
1160) -> bool {
1161    let scale = (head_dim as f32).sqrt().recip();
1162    let kv_group = q_heads / kv_heads;
1163    let mut query_matrix = vec![0.0f32; query_positions * head_dim];
1164    let mut key_matrix = vec![0.0f32; key_positions * head_dim];
1165    let mut value_transpose = vec![0.0f32; head_dim * key_positions];
1166    let mut scores = vec![0.0f32; query_positions * key_positions];
1167    let mut context = vec![0.0f32; query_positions * head_dim];
1168
1169    for q_head in 0..q_heads {
1170        let kv_head = q_head / kv_group;
1171        for query_position in 0..query_positions {
1172            let query_base = (query_position * q_heads + q_head) * head_dim;
1173            query_matrix[query_position * head_dim..(query_position + 1) * head_dim]
1174                .copy_from_slice(&queries[query_base..query_base + head_dim]);
1175        }
1176        for key_position in 0..key_positions {
1177            let key_base = (key_position * kv_heads + kv_head) * head_dim;
1178            key_matrix[key_position * head_dim..(key_position + 1) * head_dim]
1179                .copy_from_slice(&keys[key_base..key_base + head_dim]);
1180            for lane in 0..head_dim {
1181                value_transpose[lane * key_positions + key_position] = values[key_base + lane];
1182            }
1183        }
1184
1185        if !accelerate_sgemm(
1186            &query_matrix,
1187            &key_matrix,
1188            query_positions,
1189            head_dim,
1190            key_positions,
1191            0.0,
1192            false,
1193            &mut scores,
1194        ) {
1195            return false;
1196        }
1197        for query_position in 0..query_positions {
1198            let score_row =
1199                &mut scores[query_position * key_positions..(query_position + 1) * key_positions];
1200            let mask = &additive_mask
1201                [query_position * key_positions..(query_position + 1) * key_positions];
1202            for (score, mask_value) in score_row.iter_mut().zip(mask) {
1203                *score = *score * scale + mask_value;
1204            }
1205            softmax_rows_with_arithmetic(score_row, 1, key_positions, softmax_arithmetic);
1206        }
1207        if !accelerate_sgemm(
1208            &scores,
1209            &value_transpose,
1210            query_positions,
1211            key_positions,
1212            head_dim,
1213            0.0,
1214            false,
1215            &mut context,
1216        ) {
1217            return false;
1218        }
1219        for query_position in 0..query_positions {
1220            let out_base = (query_position * q_heads + q_head) * head_dim;
1221            out[out_base..out_base + head_dim].copy_from_slice(
1222                &context[query_position * head_dim..(query_position + 1) * head_dim],
1223            );
1224        }
1225    }
1226    true
1227}
1228
1229#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
1230#[allow(clippy::too_many_arguments)]
1231fn accelerate_gqa_attention(
1232    _queries: &[f32],
1233    _keys: &[f32],
1234    _values: &[f32],
1235    _additive_mask: &[f32],
1236    _query_positions: usize,
1237    _key_positions: usize,
1238    _q_heads: usize,
1239    _kv_heads: usize,
1240    _head_dim: usize,
1241    _softmax_arithmetic: F32SoftmaxArithmetic,
1242    _out: &mut [f32],
1243) -> bool {
1244    false
1245}
1246
1247#[allow(clippy::too_many_arguments)]
1248fn attention_weighted_sum(
1249    scores: &[f32],
1250    values: &[f32],
1251    kv_head: usize,
1252    kv_heads: usize,
1253    head_dim: usize,
1254    accumulation: F32LinearAccumulation,
1255    out: &mut [f32],
1256) {
1257    if accumulation == F32LinearAccumulation::WidenedF64 {
1258        for lane in 0..head_dim {
1259            let mut sum = 0.0f64;
1260            for (key_position, weight) in scores.iter().copied().enumerate() {
1261                let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1262                sum += f64::from(weight) * f64::from(value);
1263            }
1264            out[lane] = sum as f32;
1265        }
1266        return;
1267    }
1268    let lanes = match accumulation {
1269        F32LinearAccumulation::Scalar => 1,
1270        F32LinearAccumulation::Lanes4 | F32LinearAccumulation::FusedLanes4 => 4,
1271        F32LinearAccumulation::Lanes8 | F32LinearAccumulation::FusedLanes8 => 8,
1272        F32LinearAccumulation::Accelerate
1273        | F32LinearAccumulation::AccelerateRowInvariant
1274        | F32LinearAccumulation::AccelerateBiasSeeded
1275        | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => 1,
1276        F32LinearAccumulation::WidenedF64 => unreachable!("handled above"),
1277    };
1278    for lane in 0..head_dim {
1279        let mut partial = [0.0f32; 8];
1280        for (key_position, weight) in scores.iter().copied().enumerate() {
1281            let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1282            let partial_index = key_position % lanes;
1283            partial[partial_index] = match accumulation {
1284                F32LinearAccumulation::FusedLanes4 | F32LinearAccumulation::FusedLanes8 => {
1285                    weight.mul_add(value, partial[partial_index])
1286                }
1287                F32LinearAccumulation::Scalar
1288                | F32LinearAccumulation::Lanes4
1289                | F32LinearAccumulation::Lanes8
1290                | F32LinearAccumulation::Accelerate
1291                | F32LinearAccumulation::AccelerateRowInvariant
1292                | F32LinearAccumulation::AccelerateBiasSeeded
1293                | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
1294                | F32LinearAccumulation::WidenedF64 => partial[partial_index] + weight * value,
1295            };
1296        }
1297        let mut sum = 0.0f32;
1298        for value in &partial[..lanes] {
1299            sum += *value;
1300        }
1301        out[lane] = sum;
1302    }
1303}
1304
1305/// Collapse the three mRoPE axes into one `cos`/`sin` row using the checkpoint's INTERLEAVED rule.
1306///
1307/// The pinned config sets `rope_scaling.interleaved = true`, which selects a different branch from
1308/// the familiar section-split one — a difference that is numerically invisible whenever the three
1309/// axes carry equal positions (which OQ-4 says they always do here, all three receiving the same
1310/// scalar causal index) and therefore exactly the kind of thing a port gets wrong and only discovers
1311/// against a batched or genuinely multimodal input. It is implemented faithfully regardless.
1312///
1313/// `axes` is the first half of each axis's row, `[3][half]`; `out` receives `[half]`. Element `j`
1314/// takes axis `j % 3` while `j` lies in `1..sections[1..].max() * 3`, and axis 0 elsewhere.
1315///
1316/// # Panics
1317///
1318/// Panics if `out` is not `half` long or an axis row is short.
1319pub fn mrope_interleave(axes: [&[f32]; 3], sections: [usize; 3], out: &mut [f32]) {
1320    let half = out.len();
1321    for axis in axes {
1322        assert!(
1323            axis.len() >= half,
1324            "axis row shorter than the half-dimension"
1325        );
1326    }
1327
1328    // Start from axis 0 everywhere, then overwrite the strided lanes from axes 1 and 2, exactly as
1329    // the reference does with its `x_t[..., beg:end:3] = x[beg, ..., beg:end:3]` assignments.
1330    out.copy_from_slice(&axes[0][..half]);
1331    let modality_num = 3usize;
1332    for (axis_index, section) in sections.iter().enumerate().skip(1) {
1333        let end = section * modality_num;
1334        let mut lane = axis_index;
1335        while lane < end && lane < half {
1336            out[lane] = axes[axis_index][lane];
1337            lane += modality_num;
1338        }
1339    }
1340}
1341
1342/// Apply rotary embeddings to one head row in the `rotate_half` layout.
1343///
1344/// `row` is `[head_dim]`; `cos` and `sin` are the full `[head_dim]` rows (the doubled half). The
1345/// transform is `x*cos + rotate_half(x)*sin` where `rotate_half` maps `[a, b] -> [-b, a]` over the
1346/// two halves.
1347///
1348/// # Panics
1349///
1350/// Panics if `cos`/`sin` do not match `row`, or if `head_dim` is odd.
1351pub fn apply_rope_in_place(row: &mut [f32], cos: &[f32], sin: &[f32]) {
1352    let dim = row.len();
1353    assert_eq!(cos.len(), dim, "cos must match head_dim");
1354    assert_eq!(sin.len(), dim, "sin must match head_dim");
1355    assert!(dim.is_multiple_of(2), "head_dim must be even");
1356
1357    let half = dim / 2;
1358    let original: Vec<f32> = row.to_vec();
1359    for index in 0..dim {
1360        let rotated = if index < half {
1361            -original[index + half]
1362        } else {
1363            original[index - half]
1364        };
1365        row[index] = original[index] * cos[index] + rotated * sin[index];
1366    }
1367}
1368
1369#[cfg(test)]
1370mod tests {
1371    use super::*;
1372
1373    #[test]
1374    fn linear_matches_a_hand_computed_product() {
1375        // x = [[1, 2, 3]], weight = [[1, 0, -1], [2, 2, 2]] -> [1*1 + 2*0 + 3*-1, 2+4+6] = [-2, 12]
1376        let x = [1.0, 2.0, 3.0];
1377        let weight = [1.0, 0.0, -1.0, 2.0, 2.0, 2.0];
1378        let mut out = [0.0; 2];
1379        linear(&x, &weight, None, 1, 3, 2, &mut out);
1380        assert_eq!(out, [-2.0, 12.0]);
1381
1382        let mut biased = [0.0; 2];
1383        linear(&x, &weight, Some(&[10.0, -12.0]), 1, 3, 2, &mut biased);
1384        assert_eq!(biased, [8.0, 0.0]);
1385    }
1386
1387    #[test]
1388    fn torch_cascade_sum_agrees_with_the_flat_sum_when_rounding_cannot_intervene() {
1389        // Powers of two below 2^24 add without rounding, so every reduction order must produce the
1390        // same total. This proves the cascade's bookkeeping — its drains, its ILP chains, its tail
1391        // handling — visits each element exactly once, independently of any parity claim.
1392        for length in [1usize, 7, 8, 15, 16, 31, 128, 1024, 3072] {
1393            for width in [4usize, 8] {
1394                let values: Vec<f32> = (0..length).map(|index| (index % 8) as f32).collect();
1395                let flat: f32 = values.iter().sum();
1396                assert_eq!(
1397                    torch_cascade_sum(&values, width, |value| value),
1398                    flat,
1399                    "length {length}, width {width}"
1400                );
1401            }
1402        }
1403    }
1404
1405    #[test]
1406    fn torch_cascade_sum_applies_its_transform_before_accumulating() {
1407        let values = [1.0f32, 2.0, 3.0, 4.0, 5.0];
1408        assert_eq!(torch_cascade_sum(&values, 4, |value| value * value), 55.0);
1409    }
1410
1411    #[test]
1412    fn torch_cascade_sum_differs_from_a_flat_sum_once_rounding_matters() {
1413        // A large leading term followed by many small ones is exactly the case a cascade exists to
1414        // improve: the flat sum loses every small term into the large accumulator, the cascade does
1415        // not. If these ever agreed, the transcription would have collapsed into a flat sum and the
1416        // parity sweep would be comparing one order against itself.
1417        let mut values = vec![1.0f32; 1024];
1418        values[0] = 1.0e8;
1419        let flat = values.iter().fold(0.0f32, |sum, value| sum + value);
1420        assert_ne!(torch_cascade_sum(&values, 8, |value| value), flat);
1421    }
1422
1423    #[test]
1424    fn ceil_log2_matches_its_definition() {
1425        assert_eq!(ceil_log2(0), 0);
1426        assert_eq!(ceil_log2(1), 0);
1427        assert_eq!(ceil_log2(2), 1);
1428        assert_eq!(ceil_log2(3), 2);
1429        assert_eq!(ceil_log2(32), 5);
1430        assert_eq!(ceil_log2(33), 6);
1431    }
1432
1433    #[test]
1434    fn rms_norm_normalizes_and_scales() {
1435        // mean(x^2) for [3, 4] is 12.5; rsqrt(12.5 + 0) ~ 0.2828427
1436        let x = [3.0f32, 4.0];
1437        let weight = [1.0f32, 1.0];
1438        let mut out = [0.0; 2];
1439        rms_norm(&x, &weight, 0.0, 1, 2, &mut out);
1440        let expected = 12.5f32.sqrt().recip();
1441        assert!((out[0] - 3.0 * expected).abs() < 1e-6);
1442        assert!((out[1] - 4.0 * expected).abs() < 1e-6);
1443
1444        // The weight is applied per element, after scaling.
1445        let mut weighted = [0.0; 2];
1446        rms_norm(&x, &[2.0, 0.5], 0.0, 1, 2, &mut weighted);
1447        assert!((weighted[0] - 3.0 * expected * 2.0).abs() < 1e-6);
1448        assert!((weighted[1] - 4.0 * expected * 0.5).abs() < 1e-6);
1449    }
1450
1451    #[test]
1452    fn silu_mul_matches_the_definition() {
1453        let mut gate = [0.0f32, 1.0, -1.0];
1454        let up = [1.0f32, 2.0, 3.0];
1455        silu_mul_in_place(&mut gate, &up);
1456        assert_eq!(gate[0], 0.0);
1457        let silu_one = 1.0f32 / (1.0 + (-1.0f32).exp());
1458        assert!((gate[1] - silu_one * 2.0).abs() < 1e-6);
1459        let silu_neg = -1.0f32 / (1.0 + 1.0f32.exp());
1460        assert!((gate[2] - silu_neg * 3.0).abs() < 1e-6);
1461    }
1462
1463    #[test]
1464    fn softmax_rows_sums_to_one_and_is_shift_invariant() {
1465        let mut x = [1.0f32, 2.0, 3.0, 101.0, 102.0, 103.0];
1466        softmax_rows(&mut x, 2, 3);
1467        let first: f32 = x[..3].iter().sum();
1468        let second: f32 = x[3..].iter().sum();
1469        assert!((first - 1.0).abs() < 1e-6);
1470        assert!((second - 1.0).abs() < 1e-6);
1471        // Rows differing by a constant shift must produce identical distributions.
1472        for index in 0..3 {
1473            assert!((x[index] - x[index + 3]).abs() < 1e-6);
1474        }
1475    }
1476
1477    #[test]
1478    fn gqa_maps_each_query_head_to_its_kv_group() {
1479        let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (1, 1, 4, 2, 2);
1480        let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1481        let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1482        let values = [10.0f32, 11.0, 20.0, 21.0];
1483        let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1484
1485        gqa_attention(
1486            &queries,
1487            &keys,
1488            &values,
1489            &[0.0],
1490            query_positions,
1491            key_positions,
1492            q_heads,
1493            kv_heads,
1494            head_dim,
1495            &mut out,
1496        );
1497
1498        assert_eq!(&out[0..2], &[10.0, 11.0]);
1499        assert_eq!(&out[2..4], &[10.0, 11.0]);
1500        assert_eq!(&out[4..6], &[20.0, 21.0]);
1501        assert_eq!(&out[6..8], &[20.0, 21.0]);
1502    }
1503
1504    #[test]
1505    fn gqa_honors_the_additive_causal_mask() {
1506        let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (2, 2, 1, 1, 2);
1507        let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1508        let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1509        let values = [2.0f32, 4.0, 10.0, 20.0];
1510        let mask = [0.0f32, f32::NEG_INFINITY, 0.0, 0.0];
1511        let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1512
1513        gqa_attention(
1514            &queries,
1515            &keys,
1516            &values,
1517            &mask,
1518            query_positions,
1519            key_positions,
1520            q_heads,
1521            kv_heads,
1522            head_dim,
1523            &mut out,
1524        );
1525
1526        assert_eq!(&out[0..2], &[2.0, 4.0]);
1527        assert_eq!(&out[2..4], &[6.0, 12.0]);
1528    }
1529
1530    #[test]
1531    fn rope_rotates_a_known_pair() {
1532        // head_dim 2, cos = [0, 0], sin = [1, 1]: [a, b] -> [-b, a]
1533        let mut row = [3.0f32, 5.0];
1534        apply_rope_in_place(&mut row, &[0.0, 0.0], &[1.0, 1.0]);
1535        assert_eq!(row, [-5.0, 3.0]);
1536
1537        // Identity when cos = 1, sin = 0.
1538        let mut same = [3.0f32, 5.0];
1539        apply_rope_in_place(&mut same, &[1.0, 1.0], &[0.0, 0.0]);
1540        assert_eq!(same, [3.0, 5.0]);
1541    }
1542
1543    #[test]
1544    fn mrope_interleave_is_identity_when_all_axes_agree() {
1545        // OQ-4: all three axes carry the same scalar causal index in this model, so the interleave
1546        // must be a no-op on equal axes. If it is not, the lane arithmetic is wrong.
1547        let axis: Vec<f32> = (0..64).map(|value| value as f32).collect();
1548        let mut out = vec![0.0f32; 64];
1549        mrope_interleave([&axis, &axis, &axis], [24, 20, 20], &mut out);
1550        assert_eq!(out, axis);
1551    }
1552
1553    #[test]
1554    fn mrope_interleave_selects_the_documented_lanes() {
1555        let zeros = vec![0.0f32; 64];
1556        let ones = vec![1.0f32; 64];
1557        let twos = vec![2.0f32; 64];
1558        let mut out = vec![0.0f32; 64];
1559        mrope_interleave([&zeros, &ones, &twos], [24, 20, 20], &mut out);
1560
1561        // Lanes 1, 4, .. < 60 come from axis 1; lanes 2, 5, .. < 60 from axis 2; the rest stay 0,
1562        // including every lane at or above 60.
1563        for (lane, value) in out.iter().enumerate() {
1564            let expected = if lane < 60 && lane % 3 == 1 {
1565                1.0
1566            } else if lane < 60 && lane % 3 == 2 {
1567                2.0
1568            } else {
1569                0.0
1570            };
1571            assert_eq!(*value, expected, "lane {lane}");
1572        }
1573    }
1574}