Skip to main content

ferrum_kernels/attention/cpu/
transformer.rs

1//! CPU transformer layer using Accelerate sgemm.
2//!
3//! Same platform-split story as `super::mod`: the Accelerate extern block and
4//! its Linux fallback are mutually exclusive per build.
5
6#![allow(dead_code)]
7
8use crate::attention::{AttentionParams, LayerWeights, TransformerConfig};
9
10pub struct CpuKvCache {
11    pub k: Vec<f32>,
12    pub v: Vec<f32>,
13    pub len: usize,
14}
15
16impl Default for CpuKvCache {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22impl CpuKvCache {
23    pub fn new() -> Self {
24        Self {
25            k: Vec::new(),
26            v: Vec::new(),
27            len: 0,
28        }
29    }
30}
31
32pub fn cpu_layer_forward(
33    input: &[f32],
34    tokens: usize,
35    w: &LayerWeights,
36    cfg: &TransformerConfig,
37    cos: &[f32],
38    sin: &[f32],
39    kv_cache: &mut CpuKvCache,
40    pos_offset: usize,
41) -> Vec<f32> {
42    let h = cfg.hidden_size;
43    let nh = cfg.num_heads;
44    let nkv = cfg.num_kv_heads;
45    let hd = cfg.head_dim;
46    let im = cfg.intermediate_size;
47    let eps = cfg.rms_norm_eps;
48
49    // 1. Input LayerNorm
50    let ln_out = rms_norm(input, &w.input_ln_w, tokens, h, eps);
51
52    // 2. Q/K/V projections
53    let q = matmul_at_bt(&ln_out, &w.q_proj_w, tokens, nh * hd, h);
54    let k = matmul_at_bt(&ln_out, &w.k_proj_w, tokens, nkv * hd, h);
55    let v = matmul_at_bt(&ln_out, &w.v_proj_w, tokens, nkv * hd, h);
56
57    // 3. Transpose + optional QK norm + RoPE
58    // Empty norm weights = skip QK-norm (vocoder transformer has no QK-norm)
59    let mut q_r = if w.q_norm_w.is_empty() {
60        transpose_no_norm(&q, tokens, nh, hd)
61    } else {
62        transpose_and_norm(&q, tokens, nh, hd, &w.q_norm_w, eps)
63    };
64    let mut k_r = if w.k_norm_w.is_empty() {
65        transpose_no_norm(&k, tokens, nkv, hd)
66    } else {
67        transpose_and_norm(&k, tokens, nkv, hd, &w.k_norm_w, eps)
68    };
69    let v_r = transpose_no_norm(&v, tokens, nkv, hd);
70
71    apply_rope(&mut q_r, nh, tokens, hd, cos, sin, pos_offset);
72    apply_rope(&mut k_r, nkv, tokens, hd, cos, sin, pos_offset);
73
74    // 4. KV cache
75    let (full_k, full_v, kv_len) = update_kv(kv_cache, &k_r, &v_r, nkv, tokens, hd);
76
77    // 5. Fused attention (GQA handled internally — no repeat_kv needed)
78    let params = AttentionParams {
79        batch: 1,
80        num_heads: nh,
81        num_kv_heads: nkv,
82        q_len: tokens,
83        kv_len,
84        head_dim: hd,
85        causal: tokens > 1,
86        pos_offset,
87        sliding_window: 0,
88    };
89    let mut attn_out = vec![0.0f32; nh * tokens * hd];
90    super::fused_attention(&q_r, &full_k, &full_v, &mut attn_out, &params);
91
92    // 6. Transpose back + O projection
93    let attn_flat = untranspose(&attn_out, tokens, nh, hd);
94    let o_out = matmul_at_bt(&attn_flat, &w.o_proj_w, tokens, h, nh * hd);
95
96    // 7. Attn layer_scale + Residual add
97    let o_scaled = if let Some(ref scale) = w.attn_layer_scale {
98        scale_vec(&o_out, scale)
99    } else {
100        o_out
101    };
102    let hidden = add_vecs(input, &o_scaled);
103
104    // 8. Post LayerNorm + MLP
105    let post_ln = rms_norm(&hidden, &w.post_ln_w, tokens, h, eps);
106    let gate = matmul_at_bt(&post_ln, &w.gate_proj_w, tokens, im, h);
107    let up = matmul_at_bt(&post_ln, &w.up_proj_w, tokens, im, h);
108    let silu_out = silu_mul(&gate, &up);
109    let mlp_out = matmul_at_bt(&silu_out, &w.down_proj_w, tokens, h, im);
110
111    // 9. MLP layer_scale + Residual add
112    let mlp_scaled = if let Some(ref scale) = w.mlp_layer_scale {
113        scale_vec(&mlp_out, scale)
114    } else {
115        mlp_out
116    };
117    add_vecs(&hidden, &mlp_scaled)
118}
119
120// ── Helpers ─────────────────────────────────────────────────────────────
121
122#[cfg(target_os = "macos")]
123extern "C" {
124    fn cblas_sgemm(
125        order: i32,
126        ta: i32,
127        tb: i32,
128        m: i32,
129        n: i32,
130        k: i32,
131        alpha: f32,
132        a: *const f32,
133        lda: i32,
134        b: *const f32,
135        ldb: i32,
136        beta: f32,
137        c: *mut f32,
138        ldc: i32,
139    );
140    /// vDSP dot product: sum of a[i]*b[i] with SIMD acceleration
141    fn vDSP_dotpr(
142        a: *const f32,
143        a_stride: i32,
144        b: *const f32,
145        b_stride: i32,
146        result: *mut f32,
147        n: u64,
148    );
149    /// vDSP vector-scalar multiply
150    fn vDSP_vsmul(
151        a: *const f32,
152        a_stride: i32,
153        scalar: *const f32,
154        result: *mut f32,
155        r_stride: i32,
156        n: u64,
157    );
158}
159
160fn matmul_at_bt(a: &[f32], b: &[f32], m: usize, n: usize, k: usize) -> Vec<f32> {
161    let mut c = vec![0.0f32; m * n];
162    #[cfg(target_os = "macos")]
163    unsafe {
164        cblas_sgemm(
165            101,
166            111,
167            112,
168            m as i32,
169            n as i32,
170            k as i32,
171            1.0,
172            a.as_ptr(),
173            k as i32,
174            b.as_ptr(),
175            k as i32,
176            0.0,
177            c.as_mut_ptr(),
178            n as i32,
179        );
180    }
181    #[cfg(not(target_os = "macos"))]
182    for i in 0..m {
183        for j in 0..n {
184            let mut s = 0.0f64;
185            for p in 0..k {
186                s += a[i * k + p] as f64 * b[j * k + p] as f64;
187            }
188            c[i * n + j] = s as f32;
189        }
190    }
191    c
192}
193
194fn rms_norm(x: &[f32], w: &[f32], tokens: usize, dim: usize, eps: f64) -> Vec<f32> {
195    let mut out = vec![0.0f32; tokens * dim];
196    let eps_f32 = eps as f32;
197    for t in 0..tokens {
198        let row = &x[t * dim..(t + 1) * dim];
199        let o = &mut out[t * dim..(t + 1) * dim];
200        // Use vDSP_dotpr for sum-of-squares (same SIMD reduction as PyTorch on macOS ARM)
201        let sum_sq;
202        #[cfg(target_os = "macos")]
203        {
204            let mut dot = 0.0f32;
205            unsafe {
206                vDSP_dotpr(row.as_ptr(), 1, row.as_ptr(), 1, &mut dot, dim as u64);
207            }
208            sum_sq = dot;
209        }
210        #[cfg(not(target_os = "macos"))]
211        {
212            let mut v = 0.0f32;
213            for &val in row {
214                v += val * val;
215            }
216            sum_sq = v;
217        }
218        let inv = 1.0f32 / (sum_sq / dim as f32 + eps_f32).sqrt();
219        for i in 0..dim {
220            o[i] = row[i] * inv * w[i];
221        }
222    }
223    out
224}
225
226fn transpose_and_norm(
227    flat: &[f32],
228    tokens: usize,
229    heads: usize,
230    hd: usize,
231    w: &[f32],
232    eps: f64,
233) -> Vec<f32> {
234    let mut out = vec![0.0f32; heads * tokens * hd];
235    let eps_f32 = eps as f32;
236    for t in 0..tokens {
237        for hi in 0..heads {
238            let src = t * heads * hd + hi * hd;
239            let dst = hi * tokens * hd + t * hd;
240            // vDSP_dotpr for sum-of-squares (same SIMD as PyTorch)
241            let sum_sq;
242            #[cfg(target_os = "macos")]
243            {
244                let mut dot = 0.0f32;
245                unsafe {
246                    vDSP_dotpr(
247                        flat[src..].as_ptr(),
248                        1,
249                        flat[src..].as_ptr(),
250                        1,
251                        &mut dot,
252                        hd as u64,
253                    );
254                }
255                sum_sq = dot;
256            }
257            #[cfg(not(target_os = "macos"))]
258            {
259                let mut v = 0.0f32;
260                for d in 0..hd {
261                    v += flat[src + d] * flat[src + d];
262                }
263                sum_sq = v;
264            }
265            let inv = 1.0f32 / (sum_sq / hd as f32 + eps_f32).sqrt();
266            for d in 0..hd {
267                out[dst + d] = flat[src + d] * inv * w[d];
268            }
269        }
270    }
271    out
272}
273
274fn transpose_no_norm(flat: &[f32], tokens: usize, heads: usize, hd: usize) -> Vec<f32> {
275    let mut out = vec![0.0f32; heads * tokens * hd];
276    for t in 0..tokens {
277        for hi in 0..heads {
278            for d in 0..hd {
279                out[hi * tokens * hd + t * hd + d] = flat[t * heads * hd + hi * hd + d];
280            }
281        }
282    }
283    out
284}
285
286fn untranspose(data: &[f32], tokens: usize, heads: usize, hd: usize) -> Vec<f32> {
287    let mut out = vec![0.0f32; tokens * heads * hd];
288    for t in 0..tokens {
289        for hi in 0..heads {
290            for d in 0..hd {
291                out[t * heads * hd + hi * hd + d] = data[hi * tokens * hd + t * hd + d];
292            }
293        }
294    }
295    out
296}
297
298fn apply_rope(
299    data: &mut [f32],
300    heads: usize,
301    seq: usize,
302    hd: usize,
303    cos: &[f32],
304    sin: &[f32],
305    offset: usize,
306) {
307    let half = hd / 2;
308    for h in 0..heads {
309        for s in 0..seq {
310            let pos = offset + s;
311            let base = h * seq * hd + s * hd;
312            for i in 0..half {
313                let c = cos[pos * half + i];
314                let si = sin[pos * half + i];
315                let x0 = data[base + i];
316                let x1 = data[base + half + i];
317                data[base + i] = x0 * c - x1 * si;
318                data[base + half + i] = x1 * c + x0 * si;
319            }
320        }
321    }
322}
323
324fn repeat_kv(kv: &[f32], nkv: usize, n_rep: usize, seq: usize, hd: usize) -> Vec<f32> {
325    if n_rep == 1 {
326        return kv.to_vec();
327    }
328    let nh = nkv * n_rep;
329    let mut out = vec![0.0f32; nh * seq * hd];
330    for kh in 0..nkv {
331        for r in 0..n_rep {
332            let dst = kh * n_rep + r;
333            out[dst * seq * hd..(dst + 1) * seq * hd]
334                .copy_from_slice(&kv[kh * seq * hd..(kh + 1) * seq * hd]);
335        }
336    }
337    out
338}
339
340fn update_kv(
341    cache: &mut CpuKvCache,
342    k: &[f32],
343    v: &[f32],
344    nkv: usize,
345    new: usize,
346    hd: usize,
347) -> (Vec<f32>, Vec<f32>, usize) {
348    if cache.len == 0 {
349        cache.k = k.to_vec();
350        cache.v = v.to_vec();
351        cache.len = new;
352        (k.to_vec(), v.to_vec(), new)
353    } else {
354        let old = cache.len;
355        let total = old + new;
356        let mut fk = vec![0.0f32; nkv * total * hd];
357        let mut fv = vec![0.0f32; nkv * total * hd];
358        for h in 0..nkv {
359            fk[h * total * hd..h * total * hd + old * hd]
360                .copy_from_slice(&cache.k[h * old * hd..(h + 1) * old * hd]);
361            fk[h * total * hd + old * hd..h * total * hd + total * hd]
362                .copy_from_slice(&k[h * new * hd..(h + 1) * new * hd]);
363            fv[h * total * hd..h * total * hd + old * hd]
364                .copy_from_slice(&cache.v[h * old * hd..(h + 1) * old * hd]);
365            fv[h * total * hd + old * hd..h * total * hd + total * hd]
366                .copy_from_slice(&v[h * new * hd..(h + 1) * new * hd]);
367        }
368        cache.k = fk.clone();
369        cache.v = fv.clone();
370        cache.len = total;
371        (fk, fv, total)
372    }
373}
374
375/// Element-wise scale: out[i] = vec[i] * scale[i % scale.len()]
376fn scale_vec(vec: &[f32], scale: &[f32]) -> Vec<f32> {
377    let s_len = scale.len();
378    vec.iter()
379        .enumerate()
380        .map(|(i, &v)| v * scale[i % s_len])
381        .collect()
382}
383
384fn add_vecs(a: &[f32], b: &[f32]) -> Vec<f32> {
385    a.iter().zip(b).map(|(x, y)| x + y).collect()
386}
387
388fn silu_mul(gate: &[f32], up: &[f32]) -> Vec<f32> {
389    gate.iter()
390        .zip(up)
391        .map(|(g, u)| {
392            let s = g / (1.0 + (-g).exp());
393            s * u
394        })
395        .collect()
396}