Skip to main content

cherenkov/qwen4_exp/
cpu.rs

1//! CPU f32 reference forward for qwen4-exp (decode form, one token at a
2//! time). Slow by design: it is the oracle for the Metal path and mirrors
3//! the transformers implementation (modeling_qwen4_exp.py) step for step.
4//!
5//! Conventions verified on the checkpoint: RMSNorm weights are MLX-sanitized
6//! (plain `w * norm(x)`, no `1 + w`); the DeltaNet output gate is a sigmoid
7//! (`output_gate_type`); attention uses a per-head sigmoid output gate.
8
9use super::packed::Packed;
10use crate::nn;
11use crate::quant::QLinear;
12use anyhow::{Context, Result};
13use half::bf16;
14use rayon::prelude::*;
15
16pub struct HcWeights<'a> {
17    pub norm: &'a [bf16],
18    pub down: QLinear<'a>,
19    pub up: QLinear<'a>,
20    pub inject: Option<QLinear<'a>>,
21}
22
23pub struct AttnWeights<'a> {
24    pub q_proj: QLinear<'a>,
25    pub k_proj: QLinear<'a>,
26    pub v_proj: QLinear<'a>,
27    pub o_proj: QLinear<'a>,
28    pub q_norm: &'a [bf16],
29    pub k_norm: &'a [bf16],
30    pub index_qk: QLinear<'a>,
31    pub index_q_norm: &'a [bf16],
32    pub index_k_norm: &'a [bf16],
33}
34
35pub struct DeltaWeights<'a> {
36    pub in_proj_qkv: QLinear<'a>,
37    pub in_proj_z: QLinear<'a>,
38    pub in_proj_a: QLinear<'a>,
39    pub in_proj_b: QLinear<'a>,
40    pub conv1d: &'a [bf16],
41    pub a_log: &'a [bf16],
42    pub dt_bias: &'a [bf16],
43    pub norm: &'a [bf16],
44    pub out_proj: QLinear<'a>,
45}
46
47pub enum Mixer<'a> {
48    Attn(AttnWeights<'a>),
49    Delta(DeltaWeights<'a>),
50}
51
52pub struct MoeWeights<'a> {
53    /// Router, bf16 `[experts][hidden]`.
54    pub router: &'a [bf16],
55    pub shared_gate: &'a [bf16],
56    pub shared_up: QLinear<'a>,
57    pub shared_gate_proj: QLinear<'a>,
58    pub shared_down: QLinear<'a>,
59    pub record_layer: usize,
60}
61
62pub struct PleWeights<'a> {
63    pub key_proj: QLinear<'a>,
64    pub value_proj: QLinear<'a>,
65    pub norm_key: &'a [bf16],
66    pub norm_query: &'a [bf16],
67    pub norm_conv: &'a [bf16],
68    /// Depthwise dilated conv, `[channels][kernel]`.
69    pub conv1d: &'a [bf16],
70    pub kernel: usize,
71    pub dilation: usize,
72    pub multipliers: Vec<i64>,
73    pub head_offsets: Vec<u64>,
74    pub head_sizes: Vec<u64>,
75}
76
77pub struct Layer<'a> {
78    pub attn_hc: HcWeights<'a>,
79    pub mlp_hc: HcWeights<'a>,
80    pub mixer: Mixer<'a>,
81    pub moe: MoeWeights<'a>,
82    pub ple: Option<PleWeights<'a>>,
83}
84
85/// The one-layer MTP draft head: folds the next token's embedding into the
86/// trunk's wide residual, runs one attention + MoE block, collapses with
87/// its own mixer and reuses the trunk's LM head.
88pub struct MtpWeights<'a> {
89    /// RMSNorm weights in the raw (1 + w) convention: the MLX converter
90    /// left these two unshifted (checked against the bf16 checkpoint).
91    pub enorm: &'a [bf16],
92    pub hnorm: &'a [bf16],
93    pub fc_e: QLinear<'a>,
94    pub fc_h: QLinear<'a>,
95    pub layer: Layer<'a>,
96    pub mixer: HcWeights<'a>,
97}
98
99pub struct CpuModel<'a> {
100    pub p: &'a Packed,
101    pub embed: QLinear<'a>,
102    pub layers: Vec<Layer<'a>>,
103    pub final_mixer: HcWeights<'a>,
104    pub lm_head: QLinear<'a>,
105    pub mtp: Option<MtpWeights<'a>>,
106}
107
108#[derive(Default)]
109pub struct KvCache {
110    /// [t][kv_heads * head_dim]
111    pub k: Vec<f32>,
112    pub v: Vec<f32>,
113    /// Indexer keys before norm and rope, `[t][index_head_dim]`.
114    pub index_k: Vec<f32>,
115    pub len: usize,
116}
117
118pub struct DeltaState {
119    pub s: Vec<f32>,
120    pub conv: Vec<f32>,
121}
122
123pub struct PleState {
124    /// Ring of the last `span` normalized gated values, `[span][hc_hidden]`.
125    pub hist: Vec<f32>,
126    pub span: usize,
127    pub filled: usize,
128}
129
130pub struct State {
131    pub pos: usize,
132    pub tokens: Vec<u32>,
133    pub kv: Vec<KvCache>,
134    pub delta: Vec<DeltaState>,
135    pub ple: Option<PleState>,
136    /// Wide residual after the last layer (before the final mixer), the
137    /// MTP head's hidden input for the token just processed.
138    pub last_hyper: Vec<f32>,
139    /// The MTP layer's own KV cache, one entry per trunk position.
140    pub mtp_kv: KvCache,
141}
142
143fn hc<'a>(p: &'a Packed, prefix: &str, inject: bool) -> Result<HcWeights<'a>> {
144    Ok(HcWeights {
145        norm: p.bf16(&format!("{prefix}.hc_norm.weight"))?,
146        down: p.qlinear(&format!("{prefix}.input_mix_weight_down"))?,
147        up: p.qlinear(&format!("{prefix}.input_mix_weight_up"))?,
148        inject: if inject {
149            Some(p.qlinear(&format!("{prefix}.block_inject_weight"))?)
150        } else {
151            None
152        },
153    })
154}
155
156impl<'a> CpuModel<'a> {
157    pub fn load(p: &'a Packed) -> Result<Self> {
158        let c = &p.cfg;
159        let m = "language_model.model";
160        let mut layers = Vec::with_capacity(c.num_hidden_layers);
161
162        for l in 0..c.num_hidden_layers {
163            let lp = format!("{m}.layers.{l}");
164
165            layers.push(load_layer(
166                p,
167                &lp,
168                c.is_linear(l),
169                p.expert_layer(l),
170                c.ple_layer() == Some(l),
171            )?);
172        }
173
174        let mtp = if c.mtp_num_hidden_layers > 0 {
175            Some(MtpWeights {
176                enorm: p.bf16("mtp.pre_fc_norm_embedding.weight")?,
177                hnorm: p.bf16("mtp.pre_fc_norm_hidden.weight")?,
178                fc_e: p.qlinear("mtp.fc_embedding")?,
179                fc_h: p.qlinear("mtp.fc_hidden")?,
180                layer: load_layer(p, "mtp.layers.0", false, p.mtp_expert_layer(0)?, false)?,
181                mixer: hc(p, "mtp.hyper_connection_mixer", false)?,
182            })
183        } else {
184            None
185        };
186
187        Ok(CpuModel {
188            p,
189            embed: p.qlinear(&format!("{m}.embed_tokens"))?,
190            layers,
191            final_mixer: hc(p, &format!("{m}.hyper_connection_mixer"), false)?,
192            lm_head: p.qlinear("language_model.lm_head")?,
193            mtp,
194        })
195    }
196}
197
198fn load_layer<'a>(
199    p: &'a Packed,
200    lp: &str,
201    linear: bool,
202    record_layer: usize,
203    with_ple: bool,
204) -> Result<Layer<'a>> {
205    let c = &p.cfg;
206    let mixer = if linear {
207        let d = format!("{lp}.linear_attn");
208
209        Mixer::Delta(DeltaWeights {
210            in_proj_qkv: p.qlinear(&format!("{d}.in_proj_qkv"))?,
211            in_proj_z: p.qlinear(&format!("{d}.in_proj_z"))?,
212            in_proj_a: p.qlinear(&format!("{d}.in_proj_a"))?,
213            in_proj_b: p.qlinear(&format!("{d}.in_proj_b"))?,
214            conv1d: p.bf16(&format!("{d}.conv1d.weight"))?,
215            a_log: p.bf16(&format!("{d}.A_log"))?,
216            dt_bias: p.bf16(&format!("{d}.dt_bias"))?,
217            norm: p.bf16(&format!("{d}.norm.weight"))?,
218            out_proj: p.qlinear(&format!("{d}.out_proj"))?,
219        })
220    } else {
221        let a = format!("{lp}.self_attn");
222
223        Mixer::Attn(AttnWeights {
224            q_proj: p.qlinear(&format!("{a}.q_proj"))?,
225            k_proj: p.qlinear(&format!("{a}.k_proj"))?,
226            v_proj: p.qlinear(&format!("{a}.v_proj"))?,
227            o_proj: p.qlinear(&format!("{a}.o_proj"))?,
228            q_norm: p.bf16(&format!("{a}.q_norm.weight"))?,
229            k_norm: p.bf16(&format!("{a}.k_norm.weight"))?,
230            index_qk: p.qlinear(&format!("{a}.indexer.index_qk_proj"))?,
231            index_q_norm: p.bf16(&format!("{a}.indexer.q_layernorm.weight"))?,
232            index_k_norm: p.bf16(&format!("{a}.indexer.k_layernorm.weight"))?,
233        })
234    };
235    let moe = MoeWeights {
236        router: p.bf16(&format!("{lp}.mlp.gate.weight"))?,
237        shared_gate: p.bf16(&format!("{lp}.mlp.shared_expert_gate.weight"))?,
238        shared_gate_proj: p.qlinear(&format!("{lp}.mlp.shared_expert.gate_proj"))?,
239        shared_up: p.qlinear(&format!("{lp}.mlp.shared_expert.up_proj"))?,
240        shared_down: p.qlinear(&format!("{lp}.mlp.shared_expert.down_proj"))?,
241        record_layer,
242    };
243    let ple = if with_ple {
244        let pp = format!("{lp}.ple");
245        let conv = p.bf16(&format!("{pp}.conv1d.weight"))?;
246        let kernel = p.shape(&format!("{pp}.conv1d.weight"))?[2];
247        let ngram = p.ngram_metadata(&format!("{pp}.ple_embedding"))?;
248
249        Some(PleWeights {
250            key_proj: p.qlinear(&format!("{pp}.key_proj"))?,
251            value_proj: p.qlinear(&format!("{pp}.value_proj"))?,
252            norm_key: p.bf16(&format!("{pp}.norm_key.weight"))?,
253            norm_query: p.bf16(&format!("{pp}.norm_query.weight"))?,
254            norm_conv: p.bf16(&format!("{pp}.norm_conv.weight"))?,
255            conv1d: conv,
256            kernel,
257            dilation: c.ngram_size,
258            multipliers: ngram.multipliers,
259            head_offsets: ngram.head_offsets,
260            head_sizes: ngram.head_sizes,
261        })
262    } else {
263        None
264    };
265
266    Ok(Layer {
267        attn_hc: hc(p, &format!("{lp}.attn_hyper_connection"), true)?,
268        mlp_hc: hc(p, &format!("{lp}.mlp_hyper_connection"), true)?,
269        mixer,
270        moe,
271        ple,
272    })
273}
274
275impl<'a> CpuModel<'a> {
276    pub fn new_state(&self) -> State {
277        let c = &self.p.cfg;
278        let mut kv = Vec::new();
279        let mut delta = Vec::new();
280
281        for l in 0..c.num_hidden_layers {
282            if c.is_linear(l) {
283                delta.push(DeltaState {
284                    s: vec![
285                        0.0;
286                        c.linear_num_value_heads
287                            * c.linear_key_head_dim
288                            * c.linear_value_head_dim
289                    ],
290                    conv: vec![
291                        0.0;
292                        (2 * c.linear_num_key_heads * c.linear_key_head_dim
293                            + c.linear_num_value_heads * c.linear_value_head_dim)
294                            * (c.linear_conv_kernel_dim - 1)
295                    ],
296                });
297            } else {
298                kv.push(KvCache::default());
299            }
300        }
301
302        let ple = c.ple_layer().map(|_| {
303            let span = (c.ple_conv_kernel_size - 1) * c.ngram_size;
304
305            PleState {
306                hist: vec![0.0; span * c.hc_hidden()],
307                span,
308                filled: 0,
309            }
310        });
311
312        State {
313            pos: 0,
314            tokens: Vec::new(),
315            kv,
316            delta,
317            ple,
318            last_hyper: Vec::new(),
319            mtp_kv: KvCache::default(),
320        }
321    }
322
323    /// MTP draft head at trunk position `pos`: pairs the trunk's wide
324    /// residual for that position (`hyper_in`) with the embedding of the
325    /// token that follows it, appends to the head's KV cache (which must
326    /// hold exactly `pos` entries) and returns (logits for the token after
327    /// `token`, the head's wide residual for chained drafting).
328    pub fn mtp_forward(
329        &self,
330        token: u32,
331        hyper_in: &[f32],
332        pos: usize,
333        state: &mut State,
334    ) -> Result<(Vec<f32>, Vec<f32>)> {
335        let w = self.mtp.as_ref().context("no MTP head")?;
336        let c = &self.p.cfg;
337        let h = c.hidden_size;
338        let hc = c.hc_count;
339        let eps = c.rms_norm_eps as f32;
340
341        anyhow::ensure!(hyper_in.len() == h * hc, "hyper input width");
342
343        let mut e = vec![0.0f32; h];
344
345        self.embed.dequant_row(token as usize, &mut e);
346
347        let en = rms_norm_shift(&e, w.enorm, eps);
348        let mut fe = vec![0.0f32; h];
349
350        w.fc_e.matvec(&en, &mut fe);
351
352        let mut hyper = vec![0.0f32; h * hc];
353        let mut fh = vec![0.0f32; h];
354
355        for g in 0..hc {
356            let hn = rms_norm_shift(
357                &hyper_in[g * h..(g + 1) * h],
358                &w.hnorm[g * h..(g + 1) * h],
359                eps,
360            );
361
362            w.fc_h.matvec(&hn, &mut fh);
363
364            for i in 0..h {
365                hyper[g * h + i] = fe[i] + fh[i];
366            }
367        }
368
369        let layer = &w.layer;
370        let (mixed, inj) = self.gated_residual(&layer.attn_hc, &hyper, eps);
371        let out = match &layer.mixer {
372            Mixer::Attn(a) => self.attention(a, &mixed, &mut state.mtp_kv, pos)?,
373            Mixer::Delta(_) => anyhow::bail!("MTP layer is expected to be full attention"),
374        };
375
376        inject(&mut hyper, &out, &inj);
377
378        let (mixed, inj) = self.gated_residual(&layer.mlp_hc, &hyper, eps);
379        let out = self.moe(&layer.moe, &mixed);
380
381        inject(&mut hyper, &out, &inj);
382
383        let (mixed, _) = self.gated_residual(&w.mixer, &hyper, eps);
384        let mut logits = vec![0.0f32; self.lm_head.out_dim];
385
386        self.lm_head.matvec(&mixed, &mut logits);
387
388        Ok((logits, hyper))
389    }
390
391    /// Forward one token at `state.pos`; returns logits.
392    pub fn forward_token(&self, token: u32, state: &mut State) -> Result<Vec<f32>> {
393        let c = &self.p.cfg;
394        let h = c.hidden_size;
395        let hc = c.hc_count;
396        let hh = h * hc;
397        let eps = c.rms_norm_eps as f32;
398
399        let mut e = vec![0.0f32; h];
400
401        self.embed.dequant_row(token as usize, &mut e);
402
403        let mut hyper = vec![0.0f32; hh];
404
405        for g in 0..hc {
406            hyper[g * h..(g + 1) * h].copy_from_slice(&e);
407        }
408
409        state.tokens.push(token);
410
411        let mut kv_idx = 0;
412        let mut delta_idx = 0;
413
414        for layer in &self.layers {
415            if let Some(ple) = &layer.ple {
416                let add = self.ple(ple, &hyper, state)?;
417
418                for (x, a) in hyper.iter_mut().zip(&add) {
419                    *x += a;
420                }
421            }
422
423            let (mixed, inj) = self.gated_residual(&layer.attn_hc, &hyper, eps);
424            let out = match &layer.mixer {
425                Mixer::Attn(a) => {
426                    let o = self.attention(a, &mixed, &mut state.kv[kv_idx], state.pos)?;
427                    kv_idx += 1;
428
429                    o
430                }
431                Mixer::Delta(d) => {
432                    let o = self.deltanet(d, &mixed, &mut state.delta[delta_idx]);
433                    delta_idx += 1;
434
435                    o
436                }
437            };
438
439            inject(&mut hyper, &out, &inj);
440
441            let (mixed, inj) = self.gated_residual(&layer.mlp_hc, &hyper, eps);
442            let out = self.moe(&layer.moe, &mixed);
443
444            inject(&mut hyper, &out, &inj);
445        }
446
447        state.pos += 1;
448
449        state.last_hyper.clone_from(&hyper);
450
451        let (mixed, _) = self.gated_residual(&self.final_mixer, &hyper, eps);
452        let mut logits = vec![0.0f32; self.lm_head.out_dim];
453
454        self.lm_head.matvec(&mixed, &mut logits);
455
456        Ok(logits)
457    }
458
459    /// Gated residual read: returns the mixed hidden input for the block and
460    /// the per-stream injection weights (empty for the final mixer).
461    fn gated_residual(&self, w: &HcWeights, hyper: &[f32], eps: f32) -> (Vec<f32>, Vec<f32>) {
462        let c = &self.p.cfg;
463        let h = c.hidden_size;
464        let hc = c.hc_count;
465        let normed = grouped_rms_norm(hyper, w.norm, h, eps);
466        let mut d = vec![0.0f32; w.down.out_dim];
467
468        w.down.matvec(&normed, &mut d);
469
470        for v in d.iter_mut() {
471            *v = nn::silu(*v / hc as f32);
472        }
473
474        let mut u = vec![0.0f32; w.up.out_dim];
475
476        w.up.matvec(&d, &mut u);
477
478        let mut mixed = vec![0.0f32; h];
479
480        for g in 0..hc {
481            for i in 0..h {
482                mixed[i] += nn::sigmoid(u[g * h + i]) * normed[g * h + i];
483            }
484        }
485
486        for v in mixed.iter_mut() {
487            *v /= hc as f32;
488        }
489
490        let inj = match &w.inject {
491            Some(q) => {
492                let mut r = vec![0.0f32; q.out_dim];
493
494                q.matvec(&normed, &mut r);
495
496                r.iter().map(|v| 2.0 * nn::sigmoid(v / hc as f32)).collect()
497            }
498            None => Vec::new(),
499        };
500
501        (mixed, inj)
502    }
503
504    fn moe(&self, w: &MoeWeights, x: &[f32]) -> Vec<f32> {
505        let c = &self.p.cfg;
506        let h = c.hidden_size;
507        // Router: softmax over all experts in f32, top-k, renormalize.
508        let mut logits = bf16_matvec(w.router, c.num_experts, h, x);
509
510        nn::softmax(&mut logits);
511
512        let mut idx: Vec<usize> = (0..c.num_experts).collect();
513
514        idx.sort_by(|&a, &b| logits[b].total_cmp(&logits[a]).then(a.cmp(&b)));
515
516        let top: Vec<usize> = idx[..c.num_experts_per_tok].to_vec();
517        let sum: f32 = top.iter().map(|&i| logits[i]).sum();
518        let mut out = vec![0.0f32; h];
519
520        for &ei in &top {
521            let weight = if c.norm_topk_prob {
522                logits[ei] / sum
523            } else {
524                logits[ei]
525            };
526            let ex = self.p.expert(w.record_layer, ei);
527            let y = mlp(&ex.gate, &ex.up, &ex.down, x);
528
529            for (o, v) in out.iter_mut().zip(&y) {
530                *o += weight * v;
531            }
532        }
533
534        let shared = mlp(&w.shared_gate_proj, &w.shared_up, &w.shared_down, x);
535        let g = nn::sigmoid(bf16_dot(w.shared_gate, x));
536
537        for (o, v) in out.iter_mut().zip(&shared) {
538            *o += g * v;
539        }
540
541        out
542    }
543
544    fn attention(
545        &self,
546        w: &AttnWeights,
547        x: &[f32],
548        cache: &mut KvCache,
549        pos: usize,
550    ) -> Result<Vec<f32>> {
551        let c = &self.p.cfg;
552        let (nh, nkv, hd) = (c.num_attention_heads, c.num_key_value_heads, c.head_dim);
553        let rot = (hd as f64 * c.partial_rotary_factor) as usize;
554        let theta = c.rope_parameters.rope_theta as f32;
555        let eps = c.rms_norm_eps as f32;
556        let kv_row = nkv * hd;
557
558        // Indexer: cache the raw key for this position, then score blocks.
559        let ihd = c.indexer_head_dim;
560        let inh = c.indexer_n_heads;
561        let mut qk = vec![0.0f32; w.index_qk.out_dim];
562
563        w.index_qk.matvec(x, &mut qk);
564
565        let (iq, ik) = qk.split_at(inh * ihd);
566
567        cache.index_k.extend_from_slice(ik);
568
569        let mut qg = vec![0.0f32; w.q_proj.out_dim];
570        let mut k = vec![0.0f32; w.k_proj.out_dim];
571        let mut v = vec![0.0f32; w.v_proj.out_dim];
572
573        w.q_proj.matvec(x, &mut qg);
574        w.k_proj.matvec(x, &mut k);
575        w.v_proj.matvec(x, &mut v);
576
577        for hi in 0..nh {
578            let q = &mut qg[hi * 2 * hd..hi * 2 * hd + hd];
579
580            nn::rms_norm(q, w.q_norm, eps);
581            rope_partial(q, pos, rot, theta);
582        }
583
584        for hi in 0..nkv {
585            let kh = &mut k[hi * hd..(hi + 1) * hd];
586
587            nn::rms_norm(kh, w.k_norm, eps);
588            rope_partial(kh, pos, rot, theta);
589        }
590
591        cache.k.extend_from_slice(&k);
592        cache.v.extend_from_slice(&v);
593
594        cache.len += 1;
595        let t_len = cache.len;
596
597        anyhow::ensure!(t_len == pos + 1, "attention cache out of step");
598
599        // Sparse selection (QSA): all tokens while the context fits the budget.
600        let selected = self.select_tokens(w, iq, cache, pos);
601
602        let scale = (hd as f32).powf(-0.5);
603        let group = nh / nkv;
604        let mut out = vec![0.0f32; nh * hd];
605        let mut scores = vec![0.0f32; selected.len()];
606
607        for hi in 0..nh {
608            let hk = hi / group;
609            let q = &qg[hi * 2 * hd..hi * 2 * hd + hd];
610
611            for (si, &ti) in selected.iter().enumerate() {
612                let kt = &cache.k[ti * kv_row + hk * hd..ti * kv_row + (hk + 1) * hd];
613                scores[si] = scale * q.iter().zip(kt).map(|(a, b)| a * b).sum::<f32>();
614            }
615
616            nn::softmax(&mut scores);
617
618            let oh = &mut out[hi * hd..(hi + 1) * hd];
619
620            for (si, &ti) in selected.iter().enumerate() {
621                let vt = &cache.v[ti * kv_row + hk * hd..ti * kv_row + (hk + 1) * hd];
622                let p = scores[si];
623
624                for d in 0..hd {
625                    oh[d] += p * vt[d];
626                }
627            }
628
629            let gate = &qg[hi * 2 * hd + hd..(hi + 1) * 2 * hd];
630
631            for d in 0..hd {
632                oh[d] *= nn::sigmoid(gate[d]);
633            }
634        }
635
636        let mut o = vec![0.0f32; w.o_proj.out_dim];
637
638        w.o_proj.matvec(&out, &mut o);
639
640        Ok(o)
641    }
642
643    /// QSA indexer for one query at `pos` over the causal prefix: blocks of
644    /// `compress_ratio` tokens scored by relu(q . pooled_key) summed over
645    /// index heads; the top `budget / ratio` blocks plus the incomplete tail
646    /// are visible. Returns visible token indices in ascending order.
647    fn select_tokens(
648        &self,
649        w: &AttnWeights,
650        iq: &[f32],
651        cache: &KvCache,
652        pos: usize,
653    ) -> Vec<usize> {
654        let c = &self.p.cfg;
655        let t_len = pos + 1;
656        let ratio = c.indexer_compress_ratio;
657        let blocks = t_len / ratio;
658        let topk = c.indexer_budget / ratio;
659
660        if blocks <= topk {
661            return (0..t_len).collect();
662        }
663
664        let ihd = c.indexer_head_dim;
665        let inh = c.indexer_n_heads;
666        let rot = (c.head_dim as f64 * c.partial_rotary_factor) as usize;
667        let theta = c.rope_parameters.rope_theta as f32;
668        let eps = c.rms_norm_eps as f32;
669        let mut q = iq.to_vec();
670
671        for hi in 0..inh {
672            let qh = &mut q[hi * ihd..(hi + 1) * ihd];
673
674            nn::rms_norm(qh, w.index_q_norm, eps);
675            rope_partial(qh, pos, rot, theta);
676        }
677
678        let mut pooled = vec![0.0f32; ihd];
679        let mut block_scores: Vec<(f32, usize)> = Vec::with_capacity(blocks);
680
681        for b in 0..blocks {
682            pooled.fill(0.0);
683
684            for t in b * ratio..(b + 1) * ratio {
685                for (d, value) in pooled.iter_mut().enumerate() {
686                    *value += cache.index_k[t * ihd + d];
687                }
688            }
689
690            for v in pooled.iter_mut() {
691                *v /= ratio as f32;
692            }
693
694            nn::rms_norm(&mut pooled, w.index_k_norm, eps);
695            rope_partial(&mut pooled, b * ratio, rot, theta);
696
697            let mut s = 0.0f32;
698
699            for hi in 0..inh {
700                let qh = &q[hi * ihd..(hi + 1) * ihd];
701                let dot: f32 = qh.iter().zip(&pooled).map(|(a, b)| a * b).sum();
702                s += dot.max(0.0);
703            }
704
705            block_scores.push((s / (ihd as f32).sqrt(), b));
706        }
707
708        block_scores.sort_by(|a, b| b.0.total_cmp(&a.0).then(a.1.cmp(&b.1)));
709
710        let mut sel: Vec<usize> = block_scores[..topk]
711            .iter()
712            .flat_map(|&(_, b)| b * ratio..(b + 1) * ratio)
713            .collect();
714
715        sel.extend(blocks * ratio..t_len);
716        sel.sort_unstable();
717
718        sel
719    }
720
721    /// Gated DeltaNet, recurrent form, f32 state; sigmoid output gate.
722    fn deltanet(&self, w: &DeltaWeights, x: &[f32], state: &mut DeltaState) -> Vec<f32> {
723        let c = &self.p.cfg;
724        // n* counts heads; d* counts lanes within a head. Value heads
725        // share key/query heads in contiguous groups of nv / nk.
726        let (nk, nv) = (c.linear_num_key_heads, c.linear_num_value_heads);
727        let (dk, dv) = (c.linear_key_head_dim, c.linear_value_head_dim);
728        let qk_dim = nk * dk;
729        let v_dim = nv * dv;
730        let conv_dim = 2 * qk_dim + v_dim;
731        let ck = c.linear_conv_kernel_dim;
732
733        let mut qkv = vec![0.0f32; conv_dim];
734        let mut gate_input = vec![0.0f32; v_dim];
735        let mut decay_input = vec![0.0f32; nv];
736        let mut beta_input = vec![0.0f32; nv];
737
738        w.in_proj_qkv.matvec(x, &mut qkv);
739        w.in_proj_z.matvec(x, &mut gate_input);
740        w.in_proj_a.matvec(x, &mut decay_input);
741        w.in_proj_b.matvec(x, &mut beta_input);
742
743        causal_conv(&mut qkv, w.conv1d, &mut state.conv, ck);
744
745        // Normalize q and k per key head; only q receives 1/sqrt(dk).
746        let mut q = qkv[..qk_dim].to_vec();
747        let mut keys = qkv[qk_dim..2 * qk_dim].to_vec();
748        let v = &qkv[2 * qk_dim..];
749        let qscale = (dk as f32).powf(-0.5);
750
751        for hi in 0..nk {
752            let qh = &mut q[hi * dk..(hi + 1) * dk];
753
754            nn::l2_norm(qh, 1e-6);
755
756            for qv in qh.iter_mut() {
757                *qv *= qscale;
758            }
759
760            nn::l2_norm(&mut keys[hi * dk..(hi + 1) * dk], 1e-6);
761        }
762
763        let group = nv / nk;
764        let mut y = vec![0.0f32; v_dim];
765        let mut kv_mem = vec![0.0f32; dv];
766        let mut delta = vec![0.0f32; dv];
767        let sigmoid_gate = c.output_gate_type == "sigmoid";
768
769        for hv in 0..nv {
770            let hk = hv / group;
771            let qh = &q[hk * dk..(hk + 1) * dk];
772            let kh = &keys[hk * dk..(hk + 1) * dk];
773            let beta = nn::sigmoid(beta_input[hv]);
774            let g = -(w.a_log[hv].to_f32().exp())
775                * nn::softplus(decay_input[hv] + w.dt_bias[hv].to_f32());
776            let decay = g.exp();
777            // CPU state is [key_lane][value_lane]; delta_scan2 on Metal
778            // stores its transpose so each simdgroup can own one value lane.
779            let s = &mut state.s[hv * dk * dv..(hv + 1) * dk * dv];
780
781            // S <- decay*S, then read the old prediction S^T k.
782            kv_mem.fill(0.0);
783
784            for ik in 0..dk {
785                let row = &mut s[ik * dv..(ik + 1) * dv];
786                let kw = kh[ik];
787
788                for iv in 0..dv {
789                    row[iv] *= decay;
790                    kv_mem[iv] += row[iv] * kw;
791                }
792            }
793
794            // The correction is beta*(v - S^T k).
795            for iv in 0..dv {
796                delta[iv] = (v[hv * dv + iv] - kv_mem[iv]) * beta;
797            }
798
799            // Rank-one update S <- S + k*delta^T, followed by y = S^T q.
800            // Preserve these loop orders when comparing with the GPU oracle.
801            let yh = &mut y[hv * dv..(hv + 1) * dv];
802
803            for ik in 0..dk {
804                let row = &mut s[ik * dv..(ik + 1) * dv];
805                let kw = kh[ik];
806                let qw = qh[ik];
807
808                for iv in 0..dv {
809                    row[iv] += kw * delta[iv];
810                    yh[iv] += row[iv] * qw;
811                }
812            }
813
814            nn::rms_norm(yh, w.norm, 1e-6);
815
816            for iv in 0..dv {
817                let zz = gate_input[hv * dv + iv];
818                yh[iv] *= if sigmoid_gate {
819                    nn::sigmoid(zz)
820                } else {
821                    nn::silu(zz)
822                };
823            }
824        }
825
826        let mut o = vec![0.0f32; w.out_proj.out_dim];
827
828        w.out_proj.matvec(&y, &mut o);
829
830        o
831    }
832
833    fn ngram_ids(&self, w: &PleWeights, tokens: &[u32]) -> Vec<u64> {
834        Self::ngram_ids_from(
835            &self.p.cfg,
836            &w.multipliers,
837            &w.head_offsets,
838            &w.head_sizes,
839            tokens,
840        )
841    }
842
843    /// Hashed n-gram ids for the current (last) token: bigram heads then
844    /// trigram heads. A token before the current segment (past an EOS, or
845    /// before the start) reads as EOS. Multiplication wraps like torch int64.
846    pub fn ngram_ids_from(
847        c: &super::Qwen4ExpConfig,
848        multipliers: &[i64],
849        head_offsets: &[u64],
850        head_sizes: &[u64],
851        tokens: &[u32],
852    ) -> Vec<u64> {
853        let eos = c.eos_token_id;
854        let t = tokens.len() - 1;
855        let shifted: Vec<i64> = (0..c.ngram_size)
856            .map(|k| {
857                if k == 0 {
858                    return tokens[t] as i64;
859                }
860
861                if t < k || tokens[t - k..t].contains(&eos) {
862                    eos as i64
863                } else {
864                    tokens[t - k] as i64
865                }
866            })
867            .collect();
868        let mut ids = Vec::with_capacity(head_offsets.len());
869
870        for ngram in 2..=c.ngram_size {
871            let mut mixed = shifted[0].wrapping_mul(multipliers[0]);
872
873            for pos in 1..ngram {
874                mixed ^= shifted[pos].wrapping_mul(multipliers[pos]);
875            }
876
877            let start = (ngram - 2) * c.heads_per_ngram;
878
879            for head in start..start + c.heads_per_ngram {
880                let size = head_sizes[head] as i64;
881
882                ids.push(mixed.rem_euclid(size) as u64 + head_offsets[head]);
883            }
884        }
885
886        ids
887    }
888
889    /// PLE block: n-gram embedding, per-stream key/query gating, value,
890    /// dilated depthwise conv with SiLU. Returns the hc_hidden-wide addend.
891    fn ple(&self, w: &PleWeights, hyper: &[f32], state: &mut State) -> Result<Vec<f32>> {
892        let c = &self.p.cfg;
893        let h = c.hidden_size;
894        let hc = c.hc_count;
895        let hh = h * hc;
896        let eps = c.rms_norm_eps as f32;
897        let dim = self.p.manifest.ngram.dim;
898        let ids = self.ngram_ids(w, &state.tokens);
899
900        anyhow::ensure!(
901            ids.len() * dim == c.ple_embed_dim,
902            "n-gram head layout mismatch"
903        );
904
905        let mut e = vec![0.0f32; c.ple_embed_dim];
906
907        for (hi, &id) in ids.iter().enumerate() {
908            self.p.ngram_row(id, &mut e[hi * dim..(hi + 1) * dim]);
909        }
910
911        let mut key = vec![0.0f32; w.key_proj.out_dim];
912
913        w.key_proj.matvec(&e, &mut key);
914
915        let key = grouped_rms_norm(&key, w.norm_key, h, eps);
916        let mut value = vec![0.0f32; w.value_proj.out_dim];
917
918        w.value_proj.matvec(&e, &mut value);
919
920        let query = grouped_rms_norm(hyper, w.norm_query, h, eps);
921        let mut gated = vec![0.0f32; hh];
922
923        for g in 0..hc {
924            let dot: f32 = key[g * h..(g + 1) * h]
925                .iter()
926                .zip(&query[g * h..(g + 1) * h])
927                .map(|(a, b)| a * b)
928                .sum::<f32>()
929                / (h as f32).sqrt();
930            let gate = dot.abs().max(1e-6).sqrt() * dot.signum();
931            let s = nn::sigmoid(gate);
932
933            for i in 0..h {
934                gated[g * h + i] = s * value[i];
935            }
936        }
937
938        let gvn = grouped_rms_norm(&gated, w.norm_conv, h, eps);
939        // Dilated causal depthwise conv over time: tap k reads the value at
940        // t - dilation * (kernel - 1 - k); tap kernel-1 is the current value.
941        let ps = state.ple.as_mut().context("PLE state missing")?;
942        let span = ps.span;
943        let mut out = gated;
944
945        for ch in 0..hh {
946            let wrow = &w.conv1d[ch * w.kernel..(ch + 1) * w.kernel];
947            let mut acc = wrow[w.kernel - 1].to_f32() * gvn[ch];
948
949            for (k, weight) in wrow.iter().take(w.kernel - 1).enumerate() {
950                let back = w.dilation * (w.kernel - 1 - k); // 1..=span
951
952                if back <= ps.filled {
953                    let slot = (ps.filled - back) % span;
954                    acc += weight.to_f32() * ps.hist[slot * hh + ch];
955                }
956            }
957
958            out[ch] += nn::silu(acc);
959        }
960
961        let slot = ps.filled % span;
962
963        ps.hist[slot * hh..(slot + 1) * hh].copy_from_slice(&gvn);
964
965        ps.filled += 1;
966
967        Ok(out)
968    }
969}
970
971fn inject(hyper: &mut [f32], out: &[f32], inj: &[f32]) {
972    let h = out.len();
973
974    for (g, &wg) in inj.iter().enumerate() {
975        for i in 0..h {
976            hyper[g * h + i] += out[i] * wg;
977        }
978    }
979}
980
981fn mlp(gate: &QLinear, up: &QLinear, down: &QLinear, x: &[f32]) -> Vec<f32> {
982    let mut g = vec![0.0f32; gate.out_dim];
983    let mut u = vec![0.0f32; up.out_dim];
984
985    gate.matvec(x, &mut g);
986    up.matvec(x, &mut u);
987
988    for (gv, uv) in g.iter_mut().zip(&u) {
989        *gv = nn::silu(*gv) * uv;
990    }
991
992    let mut out = vec![0.0f32; down.out_dim];
993
994    down.matvec(&g, &mut out);
995
996    out
997}
998
999/// RMSNorm with the raw HF weight convention: x * inv * (1 + w).
1000fn rms_norm_shift(x: &[f32], w: &[bf16], eps: f32) -> Vec<f32> {
1001    let ms = x.iter().map(|v| v * v).sum::<f32>() / x.len() as f32;
1002    let inv = 1.0 / (ms + eps).sqrt();
1003
1004    x.iter()
1005        .zip(w)
1006        .map(|(v, w)| v * inv * (1.0 + w.to_f32()))
1007        .collect()
1008}
1009
1010/// RMSNorm applied independently to each `group`-wide slice, with a full
1011/// width weight vector.
1012fn grouped_rms_norm(x: &[f32], w: &[bf16], group: usize, eps: f32) -> Vec<f32> {
1013    debug_assert_eq!(x.len(), w.len());
1014
1015    let mut out = vec![0.0f32; x.len()];
1016
1017    for (g, chunk) in x.chunks_exact(group).enumerate() {
1018        let ms = chunk.iter().map(|v| v * v).sum::<f32>() / group as f32;
1019        let inv = 1.0 / (ms + eps).sqrt();
1020
1021        for (i, v) in chunk.iter().enumerate() {
1022            out[g * group + i] = v * inv * w[g * group + i].to_f32();
1023        }
1024    }
1025
1026    out
1027}
1028
1029fn bf16_matvec(w: &[bf16], rows: usize, cols: usize, x: &[f32]) -> Vec<f32> {
1030    debug_assert_eq!(w.len(), rows * cols);
1031    debug_assert_eq!(x.len(), cols);
1032
1033    (0..rows)
1034        .into_par_iter()
1035        .map(|r| {
1036            w[r * cols..(r + 1) * cols]
1037                .iter()
1038                .zip(x)
1039                .map(|(a, b)| a.to_f32() * b)
1040                .sum()
1041        })
1042        .collect()
1043}
1044
1045fn bf16_dot(w: &[bf16], x: &[f32]) -> f32 {
1046    w.iter().zip(x).map(|(a, b)| a.to_f32() * b).sum()
1047}
1048
1049/// Partial RoPE over the first `rot` dims with half-split pairing
1050/// (rotate_half): pair (j, j + rot/2) uses inv_freq theta^(-2j/rot).
1051fn rope_partial(x: &mut [f32], pos: usize, rot: usize, theta: f32) {
1052    let half = rot / 2;
1053
1054    for j in 0..half {
1055        let inv_freq = theta.powf(-(2.0 * j as f32) / rot as f32);
1056        let angle = pos as f32 * inv_freq;
1057        let (sin, cos) = angle.sin_cos();
1058        let a = x[j];
1059        let b = x[j + half];
1060        x[j] = a * cos - b * sin;
1061        x[j + half] = b * cos + a * sin;
1062    }
1063}
1064
1065/// Causal depthwise convolution over packed [q | k | v] channels.
1066/// History is oldest-first; preserve accumulation order for the CPU oracle.
1067fn causal_conv(qkv: &mut [f32], weights: &[bf16], history: &mut [f32], ck: usize) {
1068    let km1 = ck - 1;
1069
1070    for (ch, value) in qkv.iter_mut().enumerate() {
1071        let wrow = &weights[ch * ck..(ch + 1) * ck];
1072        let hist = &mut history[ch * km1..(ch + 1) * km1];
1073        let cur = *value;
1074        let mut acc = wrow[km1].to_f32() * cur;
1075
1076        for j in 0..km1 {
1077            acc += wrow[j].to_f32() * hist[j];
1078        }
1079
1080        hist.rotate_left(1);
1081
1082        hist[km1 - 1] = cur;
1083        *value = nn::silu(acc);
1084    }
1085}