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