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    // Team partitioning over query heads: the workers run the SAME extracted per-head loop
1003    // (`gqa_attention_head_range_with_arithmetic`), heads are independent, and no reduction
1004    // crosses a head — so the partitioned result is bit-identical to the serial reference
1005    // (`attention_partitioning_is_bit_exact` in team.rs). Gated to the default arithmetic
1006    // pair because `AttentionJob` runs exactly that; the forensic softmax and accumulation
1007    // probe orders must keep the serial path, same rule as the packed-GEMM gate in
1008    // `linear_with_accumulation`. The floor keeps decode steps over short contexts off the
1009    // dispatch (mul-add count ≈ heads × qp × kp × dim).
1010    const TEAM_ATTENTION_FLOOR_MADDS: usize = 512 * 1024;
1011    if softmax_arithmetic == F32SoftmaxArithmetic::ReciprocalMultiply
1012        && accumulation == F32LinearAccumulation::Scalar
1013        && q_heads
1014            .saturating_mul(query_positions)
1015            .saturating_mul(key_positions)
1016            .saturating_mul(head_dim)
1017            >= TEAM_ATTENTION_FLOOR_MADDS
1018        && !crate::team::thread_bypassed()
1019        && let Some(team) = crate::team::armed()
1020    {
1021        team.gqa_attention(
1022            queries,
1023            keys,
1024            values,
1025            additive_mask,
1026            query_positions,
1027            key_positions,
1028            q_heads,
1029            kv_heads,
1030            head_dim,
1031            out,
1032        );
1033        return;
1034    }
1035
1036    gqa_attention_head_range_with_arithmetic(
1037        queries,
1038        keys,
1039        values,
1040        additive_mask,
1041        query_positions,
1042        key_positions,
1043        q_heads,
1044        kv_heads,
1045        head_dim,
1046        softmax_arithmetic,
1047        accumulation,
1048        0..q_heads,
1049        out,
1050    );
1051}
1052
1053/// The scalar GQA loop restricted to `q_head_range`, writing only those heads' output spans.
1054///
1055/// This is the SAME loop [`gqa_attention_with_arithmetic`] runs — extracted, not duplicated —
1056/// so a partitioned caller (the worker team) composes the identical arithmetic per head and the
1057/// full-range serial call remains the reference. Heads are independent: no reduction crosses a
1058/// head, which is why partitioning here is bit-exact rather than merely close.
1059///
1060/// # Panics
1061///
1062/// Panics if the range exceeds `q_heads`. Full shape validation is the full-range caller's job;
1063/// partitioned callers must have validated once before splitting.
1064#[allow(clippy::too_many_arguments)]
1065pub fn gqa_attention_head_range_with_arithmetic(
1066    queries: &[f32],
1067    keys: &[f32],
1068    values: &[f32],
1069    additive_mask: &[f32],
1070    query_positions: usize,
1071    key_positions: usize,
1072    q_heads: usize,
1073    kv_heads: usize,
1074    head_dim: usize,
1075    softmax_arithmetic: F32SoftmaxArithmetic,
1076    accumulation: F32LinearAccumulation,
1077    q_head_range: std::ops::Range<usize>,
1078    out: &mut [f32],
1079) {
1080    assert!(
1081        out.len() >= query_positions * q_heads * head_dim,
1082        "attention output must hold [query_positions, q_heads, head_dim]"
1083    );
1084    let out = out.as_mut_ptr();
1085    // SAFETY: the pointer comes from the `&mut` slice above, whose length was just checked to
1086    // cover every index the head range can reach, and it is not used after this call.
1087    unsafe {
1088        gqa_attention_head_range_into(
1089            queries,
1090            keys,
1091            values,
1092            additive_mask,
1093            query_positions,
1094            key_positions,
1095            q_heads,
1096            kv_heads,
1097            head_dim,
1098            softmax_arithmetic,
1099            accumulation,
1100            q_head_range,
1101            out,
1102        );
1103    }
1104}
1105
1106/// The head-range attention loop, writing through a raw output pointer.
1107///
1108/// This exists so parallel workers never have to materialize a `&mut [f32]` over the whole output
1109/// while a sibling worker holds one too. Disjoint *writes* are not enough for that to be sound:
1110/// two live `&mut` into the same allocation is undefined behaviour whatever the access pattern,
1111/// and `rustc` marks `&mut` parameters `noalias`, so it is the optimizer — not just the model —
1112/// that the overlap would mislead. Here each worker turns the pointer into a `&mut` covering
1113/// exactly the one head span it is about to write, and those spans are disjoint by construction.
1114///
1115/// # Safety
1116///
1117/// `out` must be valid for writes across `[query_positions, q_heads, head_dim]`, and no other
1118/// reference may alias the `head_dim` spans this call's `q_head_range` writes for its duration.
1119// SAFETY: discharged by both callers — the safe wrapper asserts `out.len()` and holds the only
1120// `&mut`; the team gives each worker a disjoint `q_head_range` whose `head_dim` spans cannot
1121// overlap, and blocks until every partition reports done, so `out` outlives all writes.
1122#[allow(clippy::too_many_arguments)]
1123pub(crate) unsafe fn gqa_attention_head_range_into(
1124    queries: &[f32],
1125    keys: &[f32],
1126    values: &[f32],
1127    additive_mask: &[f32],
1128    query_positions: usize,
1129    key_positions: usize,
1130    q_heads: usize,
1131    kv_heads: usize,
1132    head_dim: usize,
1133    softmax_arithmetic: F32SoftmaxArithmetic,
1134    accumulation: F32LinearAccumulation,
1135    q_head_range: std::ops::Range<usize>,
1136    out: *mut f32,
1137) {
1138    assert!(q_head_range.end <= q_heads, "head range exceeds q_heads");
1139    let scale = (head_dim as f32).sqrt().recip();
1140    let kv_group = q_heads / kv_heads;
1141    // Per-thread scratch, not a per-call `vec!`: this runs per attention dispatch on the
1142    // steady-state decode path (doctrine: no allocator activity there). Grows monotonically
1143    // to the longest context this thread has scored.
1144    thread_local! {
1145        static SCORES_SCRATCH: std::cell::RefCell<Vec<f32>> =
1146            const { std::cell::RefCell::new(Vec::new()) };
1147    }
1148    SCORES_SCRATCH.with(|scratch| {
1149        let mut scores_guard = scratch.borrow_mut();
1150        if scores_guard.len() < key_positions {
1151            scores_guard.resize(key_positions, 0.0);
1152        }
1153        let scores = &mut scores_guard[..key_positions];
1154
1155        for query_position in 0..query_positions {
1156            let mask = &additive_mask
1157                [query_position * key_positions..(query_position + 1) * key_positions];
1158            for q_head in q_head_range.clone() {
1159                let kv_head = q_head / kv_group;
1160                let query_base = (query_position * q_heads + q_head) * head_dim;
1161                let query = &queries[query_base..query_base + head_dim];
1162                for (key_position, score) in scores.iter_mut().enumerate() {
1163                    let key_base = (key_position * kv_heads + kv_head) * head_dim;
1164                    let key = &keys[key_base..key_base + head_dim];
1165                    let dot = dot_with_accumulation(query, key, accumulation);
1166                    *score = dot * scale + mask[key_position];
1167                }
1168                softmax_rows_with_arithmetic(scores, 1, key_positions, softmax_arithmetic);
1169
1170                // SAFETY: `query_base` indexes [query_position, q_head, head_dim] inside the bounds
1171                // the caller guaranteed, and this borrow spans only this head — the one span this
1172                // partition owns, disjoint from every other partition's.
1173                let head_out =
1174                    unsafe { std::slice::from_raw_parts_mut(out.add(query_base), head_dim) };
1175                attention_weighted_sum(
1176                    scores,
1177                    values,
1178                    kv_head,
1179                    kv_heads,
1180                    head_dim,
1181                    accumulation,
1182                    head_out,
1183                );
1184            }
1185        }
1186    });
1187}
1188
1189/// Executes the two attention matrix products through the exact macOS SGEMM candidate.
1190///
1191/// This is intentionally an L2-parity probe rather than the normal attention route. The gather
1192/// buffers present the strided GQA heads as the row-major matrices consumed by CBLAS; the scalar
1193/// path above remains the cross-platform reference and the fallback when this candidate is off.
1194#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
1195#[allow(clippy::too_many_arguments)]
1196fn accelerate_gqa_attention(
1197    queries: &[f32],
1198    keys: &[f32],
1199    values: &[f32],
1200    additive_mask: &[f32],
1201    query_positions: usize,
1202    key_positions: usize,
1203    q_heads: usize,
1204    kv_heads: usize,
1205    head_dim: usize,
1206    softmax_arithmetic: F32SoftmaxArithmetic,
1207    out: &mut [f32],
1208) -> bool {
1209    let scale = (head_dim as f32).sqrt().recip();
1210    let kv_group = q_heads / kv_heads;
1211    let mut query_matrix = vec![0.0f32; query_positions * head_dim];
1212    let mut key_matrix = vec![0.0f32; key_positions * head_dim];
1213    let mut value_transpose = vec![0.0f32; head_dim * key_positions];
1214    let mut scores = vec![0.0f32; query_positions * key_positions];
1215    let mut context = vec![0.0f32; query_positions * head_dim];
1216
1217    for q_head in 0..q_heads {
1218        let kv_head = q_head / kv_group;
1219        for query_position in 0..query_positions {
1220            let query_base = (query_position * q_heads + q_head) * head_dim;
1221            query_matrix[query_position * head_dim..(query_position + 1) * head_dim]
1222                .copy_from_slice(&queries[query_base..query_base + head_dim]);
1223        }
1224        for key_position in 0..key_positions {
1225            let key_base = (key_position * kv_heads + kv_head) * head_dim;
1226            key_matrix[key_position * head_dim..(key_position + 1) * head_dim]
1227                .copy_from_slice(&keys[key_base..key_base + head_dim]);
1228            for lane in 0..head_dim {
1229                value_transpose[lane * key_positions + key_position] = values[key_base + lane];
1230            }
1231        }
1232
1233        if !accelerate_sgemm(
1234            &query_matrix,
1235            &key_matrix,
1236            query_positions,
1237            head_dim,
1238            key_positions,
1239            0.0,
1240            false,
1241            &mut scores,
1242        ) {
1243            return false;
1244        }
1245        for query_position in 0..query_positions {
1246            let score_row =
1247                &mut scores[query_position * key_positions..(query_position + 1) * key_positions];
1248            let mask = &additive_mask
1249                [query_position * key_positions..(query_position + 1) * key_positions];
1250            for (score, mask_value) in score_row.iter_mut().zip(mask) {
1251                *score = *score * scale + mask_value;
1252            }
1253            softmax_rows_with_arithmetic(score_row, 1, key_positions, softmax_arithmetic);
1254        }
1255        if !accelerate_sgemm(
1256            &scores,
1257            &value_transpose,
1258            query_positions,
1259            key_positions,
1260            head_dim,
1261            0.0,
1262            false,
1263            &mut context,
1264        ) {
1265            return false;
1266        }
1267        for query_position in 0..query_positions {
1268            let out_base = (query_position * q_heads + q_head) * head_dim;
1269            out[out_base..out_base + head_dim].copy_from_slice(
1270                &context[query_position * head_dim..(query_position + 1) * head_dim],
1271            );
1272        }
1273    }
1274    true
1275}
1276
1277#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
1278#[allow(clippy::too_many_arguments)]
1279fn accelerate_gqa_attention(
1280    _queries: &[f32],
1281    _keys: &[f32],
1282    _values: &[f32],
1283    _additive_mask: &[f32],
1284    _query_positions: usize,
1285    _key_positions: usize,
1286    _q_heads: usize,
1287    _kv_heads: usize,
1288    _head_dim: usize,
1289    _softmax_arithmetic: F32SoftmaxArithmetic,
1290    _out: &mut [f32],
1291) -> bool {
1292    false
1293}
1294
1295#[allow(clippy::too_many_arguments)]
1296fn attention_weighted_sum(
1297    scores: &[f32],
1298    values: &[f32],
1299    kv_head: usize,
1300    kv_heads: usize,
1301    head_dim: usize,
1302    accumulation: F32LinearAccumulation,
1303    out: &mut [f32],
1304) {
1305    if accumulation == F32LinearAccumulation::WidenedF64 {
1306        for lane in 0..head_dim {
1307            let mut sum = 0.0f64;
1308            for (key_position, weight) in scores.iter().copied().enumerate() {
1309                let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1310                sum += f64::from(weight) * f64::from(value);
1311            }
1312            out[lane] = sum as f32;
1313        }
1314        return;
1315    }
1316    let lanes = match accumulation {
1317        F32LinearAccumulation::Scalar => 1,
1318        F32LinearAccumulation::Lanes4 | F32LinearAccumulation::FusedLanes4 => 4,
1319        F32LinearAccumulation::Lanes8 | F32LinearAccumulation::FusedLanes8 => 8,
1320        F32LinearAccumulation::Accelerate
1321        | F32LinearAccumulation::AccelerateRowInvariant
1322        | F32LinearAccumulation::AccelerateBiasSeeded
1323        | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => 1,
1324        F32LinearAccumulation::WidenedF64 => unreachable!("handled above"),
1325    };
1326    for lane in 0..head_dim {
1327        let mut partial = [0.0f32; 8];
1328        for (key_position, weight) in scores.iter().copied().enumerate() {
1329            let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1330            let partial_index = key_position % lanes;
1331            partial[partial_index] = match accumulation {
1332                F32LinearAccumulation::FusedLanes4 | F32LinearAccumulation::FusedLanes8 => {
1333                    weight.mul_add(value, partial[partial_index])
1334                }
1335                F32LinearAccumulation::Scalar
1336                | F32LinearAccumulation::Lanes4
1337                | F32LinearAccumulation::Lanes8
1338                | F32LinearAccumulation::Accelerate
1339                | F32LinearAccumulation::AccelerateRowInvariant
1340                | F32LinearAccumulation::AccelerateBiasSeeded
1341                | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
1342                | F32LinearAccumulation::WidenedF64 => partial[partial_index] + weight * value,
1343            };
1344        }
1345        let mut sum = 0.0f32;
1346        for value in &partial[..lanes] {
1347            sum += *value;
1348        }
1349        out[lane] = sum;
1350    }
1351}
1352
1353/// Collapse the three mRoPE axes into one `cos`/`sin` row using the checkpoint's INTERLEAVED rule.
1354///
1355/// The pinned config sets `rope_scaling.interleaved = true`, which selects a different branch from
1356/// the familiar section-split one — a difference that is numerically invisible whenever the three
1357/// axes carry equal positions (which OQ-4 says they always do here, all three receiving the same
1358/// scalar causal index) and therefore exactly the kind of thing a port gets wrong and only discovers
1359/// against a batched or genuinely multimodal input. It is implemented faithfully regardless.
1360///
1361/// `axes` is the first half of each axis's row, `[3][half]`; `out` receives `[half]`. Element `j`
1362/// takes axis `j % 3` while `j` lies in `1..sections[1..].max() * 3`, and axis 0 elsewhere.
1363///
1364/// # Panics
1365///
1366/// Panics if `out` is not `half` long or an axis row is short.
1367pub fn mrope_interleave(axes: [&[f32]; 3], sections: [usize; 3], out: &mut [f32]) {
1368    let half = out.len();
1369    for axis in axes {
1370        assert!(
1371            axis.len() >= half,
1372            "axis row shorter than the half-dimension"
1373        );
1374    }
1375
1376    // Start from axis 0 everywhere, then overwrite the strided lanes from axes 1 and 2, exactly as
1377    // the reference does with its `x_t[..., beg:end:3] = x[beg, ..., beg:end:3]` assignments.
1378    out.copy_from_slice(&axes[0][..half]);
1379    let modality_num = 3usize;
1380    for (axis_index, section) in sections.iter().enumerate().skip(1) {
1381        let end = section * modality_num;
1382        let mut lane = axis_index;
1383        while lane < end && lane < half {
1384            out[lane] = axes[axis_index][lane];
1385            lane += modality_num;
1386        }
1387    }
1388}
1389
1390/// Apply rotary embeddings to one head row in the `rotate_half` layout.
1391///
1392/// `row` is `[head_dim]`; `cos` and `sin` are the full `[head_dim]` rows (the doubled half). The
1393/// transform is `x*cos + rotate_half(x)*sin` where `rotate_half` maps `[a, b] -> [-b, a]` over the
1394/// two halves.
1395///
1396/// # Panics
1397///
1398/// Panics if `cos`/`sin` do not match `row`, or if `head_dim` is odd.
1399pub fn apply_rope_in_place(row: &mut [f32], cos: &[f32], sin: &[f32]) {
1400    let dim = row.len();
1401    assert_eq!(cos.len(), dim, "cos must match head_dim");
1402    assert_eq!(sin.len(), dim, "sin must match head_dim");
1403    assert!(dim.is_multiple_of(2), "head_dim must be even");
1404
1405    let half = dim / 2;
1406    let original: Vec<f32> = row.to_vec();
1407    for index in 0..dim {
1408        let rotated = if index < half {
1409            -original[index + half]
1410        } else {
1411            original[index - half]
1412        };
1413        row[index] = original[index] * cos[index] + rotated * sin[index];
1414    }
1415}
1416
1417#[cfg(test)]
1418mod tests {
1419    use super::*;
1420
1421    #[test]
1422    fn linear_matches_a_hand_computed_product() {
1423        // x = [[1, 2, 3]], weight = [[1, 0, -1], [2, 2, 2]] -> [1*1 + 2*0 + 3*-1, 2+4+6] = [-2, 12]
1424        let x = [1.0, 2.0, 3.0];
1425        let weight = [1.0, 0.0, -1.0, 2.0, 2.0, 2.0];
1426        let mut out = [0.0; 2];
1427        linear(&x, &weight, None, 1, 3, 2, &mut out);
1428        assert_eq!(out, [-2.0, 12.0]);
1429
1430        let mut biased = [0.0; 2];
1431        linear(&x, &weight, Some(&[10.0, -12.0]), 1, 3, 2, &mut biased);
1432        assert_eq!(biased, [8.0, 0.0]);
1433    }
1434
1435    #[test]
1436    fn torch_cascade_sum_agrees_with_the_flat_sum_when_rounding_cannot_intervene() {
1437        // Powers of two below 2^24 add without rounding, so every reduction order must produce the
1438        // same total. This proves the cascade's bookkeeping — its drains, its ILP chains, its tail
1439        // handling — visits each element exactly once, independently of any parity claim.
1440        for length in [1usize, 7, 8, 15, 16, 31, 128, 1024, 3072] {
1441            for width in [4usize, 8] {
1442                let values: Vec<f32> = (0..length).map(|index| (index % 8) as f32).collect();
1443                let flat: f32 = values.iter().sum();
1444                assert_eq!(
1445                    torch_cascade_sum(&values, width, |value| value),
1446                    flat,
1447                    "length {length}, width {width}"
1448                );
1449            }
1450        }
1451    }
1452
1453    #[test]
1454    fn torch_cascade_sum_applies_its_transform_before_accumulating() {
1455        let values = [1.0f32, 2.0, 3.0, 4.0, 5.0];
1456        assert_eq!(torch_cascade_sum(&values, 4, |value| value * value), 55.0);
1457    }
1458
1459    #[test]
1460    fn torch_cascade_sum_differs_from_a_flat_sum_once_rounding_matters() {
1461        // A large leading term followed by many small ones is exactly the case a cascade exists to
1462        // improve: the flat sum loses every small term into the large accumulator, the cascade does
1463        // not. If these ever agreed, the transcription would have collapsed into a flat sum and the
1464        // parity sweep would be comparing one order against itself.
1465        let mut values = vec![1.0f32; 1024];
1466        values[0] = 1.0e8;
1467        let flat = values.iter().fold(0.0f32, |sum, value| sum + value);
1468        assert_ne!(torch_cascade_sum(&values, 8, |value| value), flat);
1469    }
1470
1471    #[test]
1472    fn ceil_log2_matches_its_definition() {
1473        assert_eq!(ceil_log2(0), 0);
1474        assert_eq!(ceil_log2(1), 0);
1475        assert_eq!(ceil_log2(2), 1);
1476        assert_eq!(ceil_log2(3), 2);
1477        assert_eq!(ceil_log2(32), 5);
1478        assert_eq!(ceil_log2(33), 6);
1479    }
1480
1481    #[test]
1482    fn rms_norm_normalizes_and_scales() {
1483        // mean(x^2) for [3, 4] is 12.5; rsqrt(12.5 + 0) ~ 0.2828427
1484        let x = [3.0f32, 4.0];
1485        let weight = [1.0f32, 1.0];
1486        let mut out = [0.0; 2];
1487        rms_norm(&x, &weight, 0.0, 1, 2, &mut out);
1488        let expected = 12.5f32.sqrt().recip();
1489        assert!((out[0] - 3.0 * expected).abs() < 1e-6);
1490        assert!((out[1] - 4.0 * expected).abs() < 1e-6);
1491
1492        // The weight is applied per element, after scaling.
1493        let mut weighted = [0.0; 2];
1494        rms_norm(&x, &[2.0, 0.5], 0.0, 1, 2, &mut weighted);
1495        assert!((weighted[0] - 3.0 * expected * 2.0).abs() < 1e-6);
1496        assert!((weighted[1] - 4.0 * expected * 0.5).abs() < 1e-6);
1497    }
1498
1499    #[test]
1500    fn silu_mul_matches_the_definition() {
1501        let mut gate = [0.0f32, 1.0, -1.0];
1502        let up = [1.0f32, 2.0, 3.0];
1503        silu_mul_in_place(&mut gate, &up);
1504        assert_eq!(gate[0], 0.0);
1505        let silu_one = 1.0f32 / (1.0 + (-1.0f32).exp());
1506        assert!((gate[1] - silu_one * 2.0).abs() < 1e-6);
1507        let silu_neg = -1.0f32 / (1.0 + 1.0f32.exp());
1508        assert!((gate[2] - silu_neg * 3.0).abs() < 1e-6);
1509    }
1510
1511    #[test]
1512    fn softmax_rows_sums_to_one_and_is_shift_invariant() {
1513        let mut x = [1.0f32, 2.0, 3.0, 101.0, 102.0, 103.0];
1514        softmax_rows(&mut x, 2, 3);
1515        let first: f32 = x[..3].iter().sum();
1516        let second: f32 = x[3..].iter().sum();
1517        assert!((first - 1.0).abs() < 1e-6);
1518        assert!((second - 1.0).abs() < 1e-6);
1519        // Rows differing by a constant shift must produce identical distributions.
1520        for index in 0..3 {
1521            assert!((x[index] - x[index + 3]).abs() < 1e-6);
1522        }
1523    }
1524
1525    #[test]
1526    fn gqa_maps_each_query_head_to_its_kv_group() {
1527        let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (1, 1, 4, 2, 2);
1528        let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1529        let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1530        let values = [10.0f32, 11.0, 20.0, 21.0];
1531        let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1532
1533        gqa_attention(
1534            &queries,
1535            &keys,
1536            &values,
1537            &[0.0],
1538            query_positions,
1539            key_positions,
1540            q_heads,
1541            kv_heads,
1542            head_dim,
1543            &mut out,
1544        );
1545
1546        assert_eq!(&out[0..2], &[10.0, 11.0]);
1547        assert_eq!(&out[2..4], &[10.0, 11.0]);
1548        assert_eq!(&out[4..6], &[20.0, 21.0]);
1549        assert_eq!(&out[6..8], &[20.0, 21.0]);
1550    }
1551
1552    #[test]
1553    fn gqa_honors_the_additive_causal_mask() {
1554        let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (2, 2, 1, 1, 2);
1555        let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1556        let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1557        let values = [2.0f32, 4.0, 10.0, 20.0];
1558        let mask = [0.0f32, f32::NEG_INFINITY, 0.0, 0.0];
1559        let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1560
1561        gqa_attention(
1562            &queries,
1563            &keys,
1564            &values,
1565            &mask,
1566            query_positions,
1567            key_positions,
1568            q_heads,
1569            kv_heads,
1570            head_dim,
1571            &mut out,
1572        );
1573
1574        assert_eq!(&out[0..2], &[2.0, 4.0]);
1575        assert_eq!(&out[2..4], &[6.0, 12.0]);
1576    }
1577
1578    #[test]
1579    fn rope_rotates_a_known_pair() {
1580        // head_dim 2, cos = [0, 0], sin = [1, 1]: [a, b] -> [-b, a]
1581        let mut row = [3.0f32, 5.0];
1582        apply_rope_in_place(&mut row, &[0.0, 0.0], &[1.0, 1.0]);
1583        assert_eq!(row, [-5.0, 3.0]);
1584
1585        // Identity when cos = 1, sin = 0.
1586        let mut same = [3.0f32, 5.0];
1587        apply_rope_in_place(&mut same, &[1.0, 1.0], &[0.0, 0.0]);
1588        assert_eq!(same, [3.0, 5.0]);
1589    }
1590
1591    #[test]
1592    fn mrope_interleave_is_identity_when_all_axes_agree() {
1593        // OQ-4: all three axes carry the same scalar causal index in this model, so the interleave
1594        // must be a no-op on equal axes. If it is not, the lane arithmetic is wrong.
1595        let axis: Vec<f32> = (0..64).map(|value| value as f32).collect();
1596        let mut out = vec![0.0f32; 64];
1597        mrope_interleave([&axis, &axis, &axis], [24, 20, 20], &mut out);
1598        assert_eq!(out, axis);
1599    }
1600
1601    #[test]
1602    fn mrope_interleave_selects_the_documented_lanes() {
1603        let zeros = vec![0.0f32; 64];
1604        let ones = vec![1.0f32; 64];
1605        let twos = vec![2.0f32; 64];
1606        let mut out = vec![0.0f32; 64];
1607        mrope_interleave([&zeros, &ones, &twos], [24, 20, 20], &mut out);
1608
1609        // Lanes 1, 4, .. < 60 come from axis 1; lanes 2, 5, .. < 60 from axis 2; the rest stay 0,
1610        // including every lane at or above 60.
1611        for (lane, value) in out.iter().enumerate() {
1612            let expected = if lane < 60 && lane % 3 == 1 {
1613                1.0
1614            } else if lane < 60 && lane % 3 == 2 {
1615                2.0
1616            } else {
1617                0.0
1618            };
1619            assert_eq!(*value, expected, "lane {lane}");
1620        }
1621    }
1622}