1use crate::pool::Pool;
32use crate::qtensor::QTensor;
33use cortiq_core::TensorDtype;
34use std::sync::OnceLock;
35use std::sync::atomic::{AtomicU64, Ordering};
36
37static PERF_GDN_FORWARD_CALLS: AtomicU64 = AtomicU64::new(0);
38static PERF_GDN_FORWARD_NS: AtomicU64 = AtomicU64::new(0);
39static PERF_GDN_BATCH_CALLS: AtomicU64 = AtomicU64::new(0);
40static PERF_GDN_BATCH_NS: AtomicU64 = AtomicU64::new(0);
41static PERF_GDN_STEP_CALLS: AtomicU64 = AtomicU64::new(0);
42static PERF_GDN_STEP_NS: AtomicU64 = AtomicU64::new(0);
43
44fn perf_enabled() -> bool {
45 static ON: OnceLock<bool> = OnceLock::new();
46 *ON.get_or_init(|| std::env::var("CMF_PERF_PROFILE").as_deref() == Ok("1"))
47}
48
49pub struct VmfPhaseWeights {
51 pub thq: QTensor,
53 pub thk: QTensor,
55 pub v_proj: QTensor,
57 pub out_proj: QTensor,
59 pub decay: Vec<f64>,
61 pub conv: Option<Vec<f32>>,
67 pub k_gate: Option<(QTensor, Vec<f32>)>,
74 pub phase_delta: bool,
79}
80
81#[derive(Clone, Copy)]
82pub struct VmfPhaseCfg {
83 pub num_heads: usize,
84 pub nphase: usize,
85 pub value_head_dim: usize,
86 pub hidden_size: usize,
87 pub phase_mass: f32,
96}
97
98impl VmfPhaseCfg {
99 pub fn state_len(&self) -> usize {
100 self.num_heads * 2 * self.nphase * self.value_head_dim
101 }
102}
103
104fn phase_delta_step_f32(
110 thq: &[f32],
111 thk: &[f32],
112 v: &[f32],
113 decay: &[f64],
114 kap: Option<&[f32]>,
115 cfg: &VmfPhaseCfg,
116 state: &mut [f32],
117 out: &mut [f32],
118) {
119 let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
120 let p2 = 2 * nph;
121 let scale = 1.0f64 / (nph as f64).sqrt();
122 debug_assert_eq!(thq.len(), nh * nph);
123 debug_assert_eq!(thk.len(), nh * nph);
124 debug_assert_eq!(v.len(), nh * dv);
125 debug_assert_eq!(decay.len(), nh * p2);
126 debug_assert!(state.len() >= nh * p2 * dv);
127 debug_assert!(out.len() >= nh * dv);
128
129 let mut r = vec![0.0f64; dv];
133 let mut key = vec![0.0f64; p2];
134 for h in 0..nh {
135 let s = &mut state[h * p2 * dv..(h + 1) * p2 * dv];
136 let thk_h = &thk[h * nph..(h + 1) * nph];
137 let thq_h = &thq[h * nph..(h + 1) * nph];
138 let vt = &v[h * dv..(h + 1) * dv];
139 let ot = &mut out[h * dv..(h + 1) * dv];
140 let dec = &decay[h * p2..(h + 1) * p2];
141 let beta = kap.map_or(1.0f64, |k| k[h] as f64);
142
143 r.fill(0.0);
147 for f in 0..p2 {
148 let kf = if f < nph {
149 scale * (thk_h[f] as f64).cos()
150 } else {
151 scale * (thk_h[f - nph] as f64).sin()
152 };
153 key[f] = kf;
154 let row = &s[f * dv..(f + 1) * dv];
155 for d in 0..dv {
156 r[d] += kf * (dec[f] * row[d] as f64);
157 }
158 }
159
160 for f in 0..p2 {
168 let kf = key[f];
169 #[cfg(target_arch = "aarch64")]
170 let qf = if f < nph {
171 scale * (thq_h[f] as f64).cos()
172 } else {
173 scale * (thq_h[f - nph] as f64).sin()
174 };
175 let row = &mut s[f * dv..(f + 1) * dv];
176 for d in 0..dv {
177 let p = dec[f] * row[d] as f64;
178 row[d] = (p + beta * kf * (vt[d] as f64 - r[d])) as f32;
179 #[cfg(target_arch = "aarch64")]
180 {
181 ot[d] += (qf * row[d] as f64) as f32;
182 }
183 }
184 }
185 #[cfg(not(target_arch = "aarch64"))]
186 for f in 0..p2 {
187 let qf = if f < nph {
188 scale * (thq_h[f] as f64).cos()
189 } else {
190 scale * (thq_h[f - nph] as f64).sin()
191 };
192 let row = &s[f * dv..(f + 1) * dv];
193 for d in 0..dv {
194 ot[d] += (qf * row[d] as f64) as f32;
195 }
196 }
197 }
198}
199
200fn phase_step(
204 thq: &[f32],
205 thk: &[f32],
206 v: &[f32],
207 decay: &[f64],
208 kap: Option<&[f32]>,
209 cfg: &VmfPhaseCfg,
210 state: &mut [f32],
211 out: &mut [f32],
212 phase_delta: bool,
213) {
214 let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
215 if phase_delta {
216 phase_delta_step_f32(thq, thk, v, decay, kap, cfg, state, out);
217 return;
218 }
219 let mscale = 1.0f64 / (1.0 + cfg.phase_mass as f64);
221 let p2 = 2 * nph;
222 for h in 0..nh {
223 let s = &mut state[h * p2 * dv..(h + 1) * p2 * dv];
224 let thk_h = &thk[h * nph..(h + 1) * nph];
225 let thq_h = &thq[h * nph..(h + 1) * nph];
226 let vt = &v[h * dv..(h + 1) * dv];
227 let ot = &mut out[h * dv..(h + 1) * dv];
228 let dec = &decay[h * p2..(h + 1) * p2];
229 let kh = kap.map_or(1.0f64, |k| k[h] as f64);
231 for f in 0..p2 {
232 let (fk, fq) = if f < nph {
234 (
235 (thk_h[f] as f64 * mscale).cos(),
236 (thq_h[f] as f64 * mscale).cos(),
237 )
238 } else {
239 (
240 (thk_h[f - nph] as f64 * mscale).sin(),
241 (thq_h[f - nph] as f64 * mscale).sin(),
242 )
243 };
244 let fkw = fk * kh;
245 let row = &mut s[f * dv..(f + 1) * dv];
246 let dcf = dec[f];
247 for d in 0..dv {
248 let cell = dcf * row[d] as f64 + fkw * vt[d] as f64;
250 row[d] = cell as f32;
251 ot[d] += (fq * cell) as f32; }
253 }
254 }
255}
256
257fn kappa_of(x: &[f32], w: &VmfPhaseWeights, nh: usize, pool: Option<&Pool>) -> Option<Vec<f32>> {
260 let (kw, kb) = w.k_gate.as_ref()?;
261 let mut k = vec![0.0f32; nh];
262 kw.matvec(x, &mut k, pool);
263 for (v, b) in k.iter_mut().zip(kb) {
264 *v = 1.0 / (1.0 + (-(*v + b)).exp());
265 }
266 Some(k)
267}
268
269fn conv_in(
273 x: &[f32],
274 w: &VmfPhaseWeights,
275 cfg: &VmfPhaseCfg,
276 state: &mut Vec<f32>,
277) -> Option<Vec<f32>> {
278 let taps = w.conv.as_ref()?;
279 let h = cfg.hidden_size;
280 let k = taps.len() / h.max(1);
281 if k < 2 || taps.len() != h * k {
282 return None;
283 }
284 let ring = (k - 1) * h;
285 let base = cfg.state_len();
286 if state.len() != base + ring {
287 let mut ns = vec![0f32; base + ring];
289 let n = state.len().min(base);
290 ns[..n].copy_from_slice(&state[..n]);
291 *state = ns;
292 }
293 let mut y = vec![0.0f32; h];
294 for c in 0..h {
295 let mut acc = taps[c * k + k - 1] * x[c];
297 for j in 0..k - 1 {
298 acc += taps[c * k + j] * state[base + j * h + c];
299 }
300 y[c] = acc;
301 }
302 conv_ring_push(x, h, base, state);
303 Some(y)
304}
305
306fn conv_ring_push(x: &[f32], h: usize, base: usize, state: &mut [f32]) {
308 let ring = state.len() - base;
309 state.copy_within(base + h.., base);
310 let at = base + ring - h;
311 state[at..at + h].copy_from_slice(&x[..h]);
312}
313
314pub fn vmf_phase_forward(
316 x: &[f32],
317 w: &VmfPhaseWeights,
318 cfg: &VmfPhaseCfg,
319 state: &mut Vec<f32>,
320 pool: Option<&Pool>,
321) -> Vec<f32> {
322 if w.conv.is_none() && state.len() != cfg.state_len() {
323 *state = vec![0f32; cfg.state_len()];
324 }
325 let xc = conv_in(x, w, cfg, state);
326 let x = xc.as_deref().unwrap_or(x);
327 let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
328
329 let mut thq = vec![0.0f32; nh * nph];
330 w.thq.matvec(x, &mut thq, pool);
331 let mut thk = vec![0.0f32; nh * nph];
332 w.thk.matvec(x, &mut thk, pool);
333 let mut v = vec![0.0f32; nh * dv];
334 w.v_proj.matvec(x, &mut v, pool);
335
336 let kap = kappa_of(x, w, nh, pool);
337 let mut o = vec![0.0f32; nh * dv];
338 phase_step(
339 &thq,
340 &thk,
341 &v,
342 &w.decay,
343 kap.as_deref(),
344 cfg,
345 state,
346 &mut o,
347 w.phase_delta,
348 );
349
350 let mut out = vec![0.0f32; cfg.hidden_size];
351 w.out_proj.matvec(&o, &mut out, pool);
352 out
353}
354
355#[allow(clippy::too_many_arguments)]
360pub fn vmf_phase_pair(
361 x1: &[f32],
362 x2: &[f32],
363 w: &VmfPhaseWeights,
364 cfg: &VmfPhaseCfg,
365 state: &mut Vec<f32>,
366 scratch: &mut Vec<f32>,
367 pool: Option<&Pool>,
368) -> (Vec<f32>, Vec<f32>) {
369 if w.conv.is_none() && state.len() != cfg.state_len() {
370 *state = vec![0f32; cfg.state_len()];
371 }
372 let xc1 = conv_in(x1, w, cfg, state);
375 let x1 = xc1.as_deref().unwrap_or(x1);
376 let (xc2, x2raw) = if w.conv.is_some() {
377 let mut tmp = state.clone();
378 (conv_in(x2, w, cfg, &mut tmp), Some(x2))
379 } else {
380 (None, None)
381 };
382 let x2 = xc2.as_deref().unwrap_or(x2);
383 let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
384
385 let mut thq1 = vec![0.0f32; nh * nph];
386 let mut thq2 = vec![0.0f32; nh * nph];
387 w.thq.matvec2(x1, x2, &mut thq1, &mut thq2, pool);
388 let mut thk1 = vec![0.0f32; nh * nph];
389 let mut thk2 = vec![0.0f32; nh * nph];
390 w.thk.matvec2(x1, x2, &mut thk1, &mut thk2, pool);
391 let mut v1 = vec![0.0f32; nh * dv];
392 let mut v2 = vec![0.0f32; nh * dv];
393 w.v_proj.matvec2(x1, x2, &mut v1, &mut v2, pool);
394
395 let kap1 = kappa_of(x1, w, nh, pool);
397 let mut o1 = vec![0.0f32; nh * dv];
398 phase_step(
399 &thq1,
400 &thk1,
401 &v1,
402 &w.decay,
403 kap1.as_deref(),
404 cfg,
405 state,
406 &mut o1,
407 w.phase_delta,
408 );
409
410 let kap2 = kappa_of(x2, w, nh, pool);
412 scratch.clear();
413 scratch.extend_from_slice(state);
414 let mut o2 = vec![0.0f32; nh * dv];
415 phase_step(
416 &thq2,
417 &thk2,
418 &v2,
419 &w.decay,
420 kap2.as_deref(),
421 cfg,
422 scratch,
423 &mut o2,
424 w.phase_delta,
425 );
426 if let Some(xr) = x2raw {
429 conv_ring_push(xr, cfg.hidden_size, cfg.state_len(), scratch);
430 }
431
432 let mut out1 = vec![0.0f32; cfg.hidden_size];
433 let mut out2 = vec![0.0f32; cfg.hidden_size];
434 w.out_proj.matvec2(&o1, &o2, &mut out1, &mut out2, pool);
435 (out1, out2)
436}
437
438pub struct GdnWeights {
443 pub in_proj_qkv: QTensor,
445 pub in_proj_z: QTensor,
447 pub in_proj_a: QTensor,
449 pub in_proj_b: QTensor,
451 pub conv1d: Vec<f32>,
453 pub a_log: Vec<f32>,
455 pub dt_bias: Vec<f32>,
457 pub norm: Vec<f32>,
459 pub out_proj: QTensor,
461}
462
463#[derive(Clone, Copy)]
464pub struct GdnCfg {
465 pub num_v_heads: usize,
466 pub num_k_heads: usize,
467 pub key_head_dim: usize,
468 pub value_head_dim: usize,
469 pub conv_kernel: usize,
470 pub hidden_size: usize,
471 pub rms_eps: f64,
472 pub output_gate_sigmoid: bool,
475}
476
477impl GdnCfg {
478 pub fn conv_dim(&self) -> usize {
479 2 * self.num_k_heads * self.key_head_dim + self.num_v_heads * self.value_head_dim
480 }
481
482 pub fn state_len(&self) -> usize {
485 (self.conv_kernel - 1) * self.conv_dim()
486 + self.num_v_heads * self.key_head_dim * self.value_head_dim
487 }
488}
489
490fn softplus(x: f64) -> f64 {
491 if x > 20.0 { x } else { x.exp().ln_1p() }
492}
493
494fn sigmoid(x: f64) -> f64 {
495 1.0 / (1.0 + (-x).exp())
496}
497
498fn silu(x: f64) -> f64 {
499 x / (1.0 + (-x).exp())
500}
501
502#[derive(Clone, Copy)]
505struct SendMutF32(*mut f32);
506unsafe impl Send for SendMutF32 {}
507unsafe impl Sync for SendMutF32 {}
508
509#[allow(clippy::too_many_arguments)]
522fn gdn_step(
523 qkv: &[f32],
524 z: &[f32],
525 a: &[f32],
526 b: &[f32],
527 w: &GdnWeights,
528 cfg: &GdnCfg,
529 state: &mut [f32],
530 of: &mut [f32],
531 pool: Option<&Pool>,
532) {
533 let perf_t0 = perf_enabled().then(std::time::Instant::now);
534 let (nv, nk, dk, dv, kk) = (
535 cfg.num_v_heads,
536 cfg.num_k_heads,
537 cfg.key_head_dim,
538 cfg.value_head_dim,
539 cfg.conv_kernel,
540 );
541 let c_dim = cfg.conv_dim();
542 let (kd, rep) = (nk * dk, nv / nk);
543 let (ring, s_all) = state.split_at_mut((kk - 1) * c_dim);
544
545 let mut cq = vec![0f32; c_dim];
549 for c in 0..c_dim {
550 let taps = &w.conv1d[c * kk..(c + 1) * kk];
551 let mut acc = qkv[c] as f64 * taps[kk - 1] as f64;
552 for j in 0..kk - 1 {
553 acc += ring[j * c_dim + c] as f64 * taps[j] as f64;
554 }
555 cq[c] = silu(acc) as f32;
556 }
557 if kk > 1 {
559 ring.copy_within(c_dim.., 0);
560 let tail = (kk - 2) * c_dim;
561 ring[tail..tail + c_dim].copy_from_slice(&qkv[..c_dim]);
562 }
563
564 let cq = &cq;
565 let s_ptr = SendMutF32(s_all.as_mut_ptr());
566 let of_ptr = SendMutF32(of.as_mut_ptr());
567 let head_range = |h0: usize, h1: usize| {
568 let (s_ptr, of_ptr) = (s_ptr, of_ptr);
571 let mut kv = crate::attention::take_buf(dv);
573 let mut delta = crate::attention::take_buf(dv);
574 let mut o = crate::attention::take_buf(dv);
575 let mut kf = crate::attention::take_buf(dk);
576 let mut qf = crate::attention::take_buf(dk);
577 for h in h0..h1 {
578 let ko = h / rep; let (qs, ks) = (ko * dk, kd + ko * dk);
580 let (mut nq, mut nkn) = (0f64, 0f64);
582 for d in 0..dk {
583 nq += (cq[qs + d] as f64) * (cq[qs + d] as f64);
584 nkn += (cq[ks + d] as f64) * (cq[ks + d] as f64);
585 }
586 let invq = (1.0 / ((nq + 1e-6).sqrt() * (dk as f64).sqrt())) as f32;
587 let invk = (1.0 / (nkn + 1e-6).sqrt()) as f32;
588 for d in 0..dk {
589 qf[d] = cq[qs + d] * invq;
590 kf[d] = cq[ks + d] * invk;
591 }
592
593 let g = (-(w.a_log[h] as f64).exp() * softplus(a[h] as f64 + w.dt_bias[h] as f64)).exp()
594 as f32;
595 let beta = sigmoid(b[h] as f64) as f32;
596
597 let s = unsafe { std::slice::from_raw_parts_mut(s_ptr.0.add(h * dk * dv), dk * dv) };
599 let oh = unsafe { std::slice::from_raw_parts_mut(of_ptr.0.add(h * dv), dv) };
600 let vt = &cq[2 * kd + h * dv..2 * kd + (h + 1) * dv];
601
602 kv[..dv].fill(0.0);
606 for di in 0..dk {
607 let kfd = kf[di];
608 let row = &s[di * dv..(di + 1) * dv];
609 for dj in 0..dv {
610 kv[dj] += row[dj] * kfd; }
612 }
613 for dj in 0..dv {
614 delta[dj] = (vt[dj] - g * kv[dj]) * beta;
615 }
616 o[..dv].fill(0.0);
617 for di in 0..dk {
618 let kfd = kf[di];
619 let qfd = qf[di];
620 let row = &mut s[di * dv..(di + 1) * dv];
621 for dj in 0..dv {
622 let cell = g * row[dj] + kfd * delta[dj];
623 row[dj] = cell;
624 o[dj] += qfd * cell; }
626 }
627 let ss: f64 = o[..dv].iter().map(|&v| (v as f64) * (v as f64)).sum();
629 let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
630 for dj in 0..dv {
631 let gate = if cfg.output_gate_sigmoid {
632 sigmoid(z[h * dv + dj] as f64)
633 } else {
634 silu(z[h * dv + dj] as f64)
635 };
636 oh[dj] = ((o[dj] as f64 * inv) * w.norm[dj] as f64 * gate) as f32;
637 }
638 }
639 crate::attention::recycle_buf(&mut kv);
640 crate::attention::recycle_buf(&mut delta);
641 crate::attention::recycle_buf(&mut o);
642 crate::attention::recycle_buf(&mut kf);
643 crate::attention::recycle_buf(&mut qf);
644 };
645 match pool {
646 Some(pool) if nv >= 4 => pool.run(&|widx, n| {
647 let chunk = nv.div_ceil(n);
648 let h0 = (widx * chunk).min(nv);
649 let h1 = (h0 + chunk).min(nv);
650 if h0 < h1 {
651 head_range(h0, h1);
652 }
653 }),
654 _ => head_range(0, nv),
655 }
656 if let Some(t0) = perf_t0 {
657 PERF_GDN_STEP_CALLS.fetch_add(1, Ordering::Relaxed);
658 PERF_GDN_STEP_NS.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
659 }
660}
661
662fn gdn_scan_on() -> bool {
665 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
666 *ON.get_or_init(|| std::env::var("CMF_GDN_SCAN").as_deref() != Ok("0"))
667}
668
669#[allow(clippy::too_many_arguments)]
680fn gdn_scan_batch(
681 qkv: &[f32],
682 z: &[f32],
683 a: &[f32],
684 bb: &[f32],
685 b: usize,
686 w: &GdnWeights,
687 cfg: &GdnCfg,
688 state: &mut [f32],
689 of: &mut [f32],
690 pool: Option<&Pool>,
691) {
692 let (nv, nk, dk, dv, kk) = (
693 cfg.num_v_heads,
694 cfg.num_k_heads,
695 cfg.key_head_dim,
696 cfg.value_head_dim,
697 cfg.conv_kernel,
698 );
699 let c_dim = cfg.conv_dim();
700 let vd = nv * dv;
701 let (kd, rep) = (nk * dk, nv / nk);
702 let hist = kk.saturating_sub(1);
703 let (ring, s_all) = state.split_at_mut(hist * c_dim);
704
705 let mut cq = vec![0f32; b * c_dim];
710 {
711 let ring_ro: &[f32] = ring;
712 let cq_ptr = SendMutF32(cq.as_mut_ptr());
713 let conv_range = |c0: usize, c1: usize| {
714 let cq_ptr = cq_ptr;
715 for c in c0..c1 {
716 let taps = &w.conv1d[c * kk..(c + 1) * kk];
717 for t in 0..b {
718 let mut acc = qkv[t * c_dim + c] as f64 * taps[kk - 1] as f64;
719 for j in 0..hist {
720 let raw = if t + j >= hist {
721 qkv[(t + j - hist) * c_dim + c]
722 } else {
723 ring_ro[(t + j) * c_dim + c]
724 };
725 acc += raw as f64 * taps[j] as f64;
726 }
727 unsafe { *cq_ptr.0.add(t * c_dim + c) = silu(acc) as f32 };
729 }
730 }
731 };
732 match pool {
733 Some(pool) if c_dim >= 256 => pool.run(&|widx, n| {
734 let chunk = c_dim.div_ceil(n);
735 let c0 = (widx * chunk).min(c_dim);
736 let c1 = (c0 + chunk).min(c_dim);
737 if c0 < c1 {
738 conv_range(c0, c1);
739 }
740 }),
741 _ => conv_range(0, c_dim),
742 }
743 }
744 if hist > 0 {
747 let mut next = vec![0f32; hist * c_dim];
748 for j in 0..hist {
749 let src = if b + j >= hist {
750 &qkv[(b + j - hist) * c_dim..(b + j - hist + 1) * c_dim]
751 } else {
752 &ring[(b + j) * c_dim..(b + j + 1) * c_dim]
753 };
754 next[j * c_dim..(j + 1) * c_dim].copy_from_slice(src);
755 }
756 ring.copy_from_slice(&next);
757 }
758
759 let cq = &cq;
760 let s_ptr = SendMutF32(s_all.as_mut_ptr());
761 let of_ptr = SendMutF32(of.as_mut_ptr());
762 let head_range = |h0: usize, h1: usize| {
763 let (s_ptr, of_ptr) = (s_ptr, of_ptr);
764 let mut kv = crate::attention::take_buf(dv);
765 let mut delta = crate::attention::take_buf(dv);
766 let mut o = crate::attention::take_buf(dv);
767 let mut kf = crate::attention::take_buf(dk);
768 let mut qf = crate::attention::take_buf(dk);
769 for h in h0..h1 {
770 let ko = h / rep;
771 let (qs, ks) = (ko * dk, kd + ko * dk);
772 let g_base = -(w.a_log[h] as f64).exp();
773 let s = unsafe { std::slice::from_raw_parts_mut(s_ptr.0.add(h * dk * dv), dk * dv) };
775 for t in 0..b {
776 let cqt = &cq[t * c_dim..(t + 1) * c_dim];
777 let (mut nq, mut nkn) = (0f64, 0f64);
778 for d in 0..dk {
779 nq += (cqt[qs + d] as f64) * (cqt[qs + d] as f64);
780 nkn += (cqt[ks + d] as f64) * (cqt[ks + d] as f64);
781 }
782 let invq = (1.0 / ((nq + 1e-6).sqrt() * (dk as f64).sqrt())) as f32;
783 let invk = (1.0 / (nkn + 1e-6).sqrt()) as f32;
784 for d in 0..dk {
785 qf[d] = cqt[qs + d] * invq;
786 kf[d] = cqt[ks + d] * invk;
787 }
788 let g = (g_base * softplus(a[t * nv + h] as f64 + w.dt_bias[h] as f64)).exp() as f32;
789 let beta = sigmoid(bb[t * nv + h] as f64) as f32;
790 let oh = unsafe { std::slice::from_raw_parts_mut(of_ptr.0.add(t * vd + h * dv), dv) };
791 let vt = &cqt[2 * kd + h * dv..2 * kd + (h + 1) * dv];
792 kv[..dv].fill(0.0);
793 for di in 0..dk {
794 let kfd = kf[di];
795 let row = &s[di * dv..(di + 1) * dv];
796 for dj in 0..dv {
797 kv[dj] += row[dj] * kfd;
798 }
799 }
800 for dj in 0..dv {
801 delta[dj] = (vt[dj] - g * kv[dj]) * beta;
802 }
803 o[..dv].fill(0.0);
804 for di in 0..dk {
805 let kfd = kf[di];
806 let qfd = qf[di];
807 let row = &mut s[di * dv..(di + 1) * dv];
808 for dj in 0..dv {
809 let cell = g * row[dj] + kfd * delta[dj];
810 row[dj] = cell;
811 o[dj] += qfd * cell;
812 }
813 }
814 let ss: f64 = o[..dv].iter().map(|&v| (v as f64) * (v as f64)).sum();
815 let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
816 let zt = &z[t * vd..(t + 1) * vd];
817 for dj in 0..dv {
818 let gate = if cfg.output_gate_sigmoid {
819 sigmoid(zt[h * dv + dj] as f64)
820 } else {
821 silu(zt[h * dv + dj] as f64)
822 };
823 oh[dj] = ((o[dj] as f64 * inv) * w.norm[dj] as f64 * gate) as f32;
824 }
825 }
826 }
827 crate::attention::recycle_buf(&mut kv);
828 crate::attention::recycle_buf(&mut delta);
829 crate::attention::recycle_buf(&mut o);
830 crate::attention::recycle_buf(&mut kf);
831 crate::attention::recycle_buf(&mut qf);
832 };
833 match pool {
834 Some(pool) if nv >= 4 => pool.run(&|widx, n| {
835 let chunk = nv.div_ceil(n);
836 let h0 = (widx * chunk).min(nv);
837 let h1 = (h0 + chunk).min(nv);
838 if h0 < h1 {
839 head_range(h0, h1);
840 }
841 }),
842 _ => head_range(0, nv),
843 }
844}
845
846pub fn gdn_forward(
848 x: &[f32],
849 w: &GdnWeights,
850 cfg: &GdnCfg,
851 state: &mut Vec<f32>,
852 pool: Option<&Pool>,
853) -> Vec<f32> {
854 let perf_t0 = perf_enabled().then(std::time::Instant::now);
855 if state.len() != cfg.state_len() {
856 *state = vec![0f32; cfg.state_len()];
857 }
858 let (c_dim, vd) = (cfg.conv_dim(), cfg.num_v_heads * cfg.value_head_dim);
859
860 let mut qkv = vec![0.0f32; c_dim];
861 let mut z = vec![0.0f32; vd];
862 let mut a = vec![0.0f32; cfg.num_v_heads];
863 let mut b = vec![0.0f32; cfg.num_v_heads];
864 let cpu_projs = |qkv: &mut Vec<f32>, z: &mut Vec<f32>, a: &mut Vec<f32>, b: &mut Vec<f32>| {
868 QTensor::matvec_many(
869 [&w.in_proj_qkv, &w.in_proj_z, &w.in_proj_a, &w.in_proj_b],
870 x,
871 [
872 qkv.as_mut_slice(),
873 z.as_mut_slice(),
874 a.as_mut_slice(),
875 b.as_mut_slice(),
876 ],
877 pool,
878 );
879 };
880 let mut done = false;
881 if crate::gpu::enabled_here() && gdn_projs_eligible(w) {
882 match crate::gpu::probe_arm(crate::gpu::OpClass::Batch) {
883 crate::gpu::ProbeArm::Gpu => {
884 let t0 = std::time::Instant::now();
885 if gdn_projs_gpu(w, x, &mut qkv, &mut z) {
886 crate::gpu::probe_record(crate::gpu::OpClass::Batch, true, t0.elapsed());
887 w.in_proj_a.matvec(x, &mut a, pool);
888 w.in_proj_b.matvec(x, &mut b, pool);
889 done = true;
890 } else {
891 crate::gpu::probe_note_decline(crate::gpu::OpClass::Batch);
892 }
893 }
894 crate::gpu::ProbeArm::CpuTimed => {
895 let t0 = std::time::Instant::now();
896 crate::gpu::cpu_scope(|| cpu_projs(&mut qkv, &mut z, &mut a, &mut b));
897 crate::gpu::probe_record(crate::gpu::OpClass::Batch, false, t0.elapsed());
898 done = true;
899 }
900 crate::gpu::ProbeArm::Cpu => {
901 crate::gpu::cpu_scope(|| cpu_projs(&mut qkv, &mut z, &mut a, &mut b));
902 done = true;
903 }
904 }
905 }
906 if !done {
907 cpu_projs(&mut qkv, &mut z, &mut a, &mut b);
908 }
909
910 let mut of = vec![0.0f32; vd];
911 gdn_step(&qkv, &z, &a, &b, w, cfg, state, &mut of, pool);
912
913 let mut out = vec![0.0f32; cfg.hidden_size];
914 w.out_proj.matvec(&of, &mut out, pool);
915 if let Some(t0) = perf_t0 {
916 PERF_GDN_FORWARD_CALLS.fetch_add(1, Ordering::Relaxed);
917 PERF_GDN_FORWARD_NS.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
918 }
919 out
920}
921
922pub fn gdn_forward_batch(
927 xs: &[f32],
928 b: usize,
929 w: &GdnWeights,
930 cfg: &GdnCfg,
931 state: &mut Vec<f32>,
932 pool: Option<&Pool>,
933) -> Vec<f32> {
934 let perf_t0 = perf_enabled().then(std::time::Instant::now);
935 if state.len() != cfg.state_len() {
936 *state = vec![0f32; cfg.state_len()];
937 }
938 let (c_dim, vd) = (cfg.conv_dim(), cfg.num_v_heads * cfg.value_head_dim);
939 let nv = cfg.num_v_heads;
940
941 let mut qkv = vec![0.0f32; b * c_dim];
942 w.in_proj_qkv.matmat(xs, b, &mut qkv, pool);
943 let mut z = vec![0.0f32; b * vd];
944 w.in_proj_z.matmat(xs, b, &mut z, pool);
945 let mut a = vec![0.0f32; b * nv];
946 w.in_proj_a.matmat(xs, b, &mut a, pool);
947 let mut bb = vec![0.0f32; b * nv];
948 w.in_proj_b.matmat(xs, b, &mut bb, pool);
949
950 let mut of = vec![0.0f32; b * vd];
951 if b > 1 && gdn_scan_on() {
952 gdn_scan_batch(&qkv, &z, &a, &bb, b, w, cfg, state, &mut of, pool);
953 } else {
954 for bi in 0..b {
955 gdn_step(
956 &qkv[bi * c_dim..(bi + 1) * c_dim],
957 &z[bi * vd..(bi + 1) * vd],
958 &a[bi * nv..(bi + 1) * nv],
959 &bb[bi * nv..(bi + 1) * nv],
960 w,
961 cfg,
962 state,
963 &mut of[bi * vd..(bi + 1) * vd],
964 pool,
965 );
966 }
967 }
968 let mut out = vec![0.0f32; b * cfg.hidden_size];
969 w.out_proj.matmat(&of, b, &mut out, pool);
970 if std::env::var("CMF_GDN_TRACE").is_ok() {
971 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
972 eprintln!(
973 "gdn-batch b={b}: |x0|={:.5} |qkv0|={:.5} |z0|={:.5} |a0|={:.5} |b0|={:.5} |of0|={:.5} |out0|={:.5} |state|={:.5}",
974 n(&xs[..cfg.hidden_size]),
975 n(&qkv[..c_dim]),
976 n(&z[..vd]),
977 n(&a[..nv]),
978 n(&bb[..nv]),
979 n(&of[..vd]),
980 n(&out[..cfg.hidden_size]),
981 n(state)
982 );
983 }
984 if let Some(t0) = perf_t0 {
985 PERF_GDN_BATCH_CALLS.fetch_add(1, Ordering::Relaxed);
986 PERF_GDN_BATCH_NS.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
987 }
988 out
989}
990
991pub struct GdnPreviewWeights {
993 pub conv_right: Vec<f32>,
996 pub logit_scale: Vec<f32>,
998 pub history_bias: Vec<f32>,
1000 pub history_c: Vec<f32>,
1002}
1003
1004#[allow(clippy::too_many_arguments)]
1016pub fn gdn_preview_batch(
1017 xs: &[f32],
1018 b: usize,
1019 w: &GdnWeights,
1020 pw: &GdnPreviewWeights,
1021 cfg: &GdnCfg,
1022 state: &[f32],
1023 pool: Option<&Pool>,
1024) -> Vec<f32> {
1025 let (nv, nk, dk, dv, kk) = (
1026 cfg.num_v_heads,
1027 cfg.num_k_heads,
1028 cfg.key_head_dim,
1029 cfg.value_head_dim,
1030 cfg.conv_kernel,
1031 );
1032 let (c_dim, vd) = (cfg.conv_dim(), nv * dv);
1033 let (kd, rep) = (nk * dk, nv / nk);
1034 let mut qkv = vec![0.0f32; b * c_dim];
1035 w.in_proj_qkv.matmat(xs, b, &mut qkv, pool);
1036 let mut z = vec![0.0f32; b * vd];
1037 w.in_proj_z.matmat(xs, b, &mut z, pool);
1038 let mut a = vec![0.0f32; b * nv];
1039 w.in_proj_a.matmat(xs, b, &mut a, pool);
1040
1041 let zeros;
1043 let state = if state.len() == cfg.state_len() {
1044 state
1045 } else {
1046 zeros = vec![0f32; cfg.state_len()];
1047 &zeros
1048 };
1049 let (ring, s_all) = state.split_at((kk - 1) * c_dim);
1050
1051 let mut cq = vec![0.0f32; b * c_dim];
1055 let cq_ptr = SendMutF32(cq.as_mut_ptr());
1056 let qkv_r = &qkv;
1057 let conv = |c0: usize, c1: usize| {
1058 let cq_ptr = cq_ptr;
1059 for c in c0..c1 {
1060 let taps = &w.conv1d[c * kk..(c + 1) * kk];
1061 for t in 0..b {
1062 let mut acc = 0f64;
1063 for (j, &tap) in taps.iter().enumerate() {
1064 let i = t + j; let x = if i < kk - 1 {
1066 ring[i * c_dim + c]
1067 } else {
1068 qkv_r[(i - (kk - 1)) * c_dim + c]
1069 };
1070 acc += x as f64 * tap as f64;
1071 }
1072 for d in 1..kk.min(b - t) {
1073 acc += qkv_r[(t + d) * c_dim + c] as f64
1074 * pw.conv_right[c * (kk - 1) + d - 1] as f64;
1075 }
1076 unsafe { *cq_ptr.0.add(t * c_dim + c) = silu(acc) as f32 };
1078 }
1079 }
1080 };
1081 match pool {
1082 Some(p) => p.run_rows(c_dim, &conv),
1083 None => conv(0, c_dim),
1084 }
1085
1086 let mut qn = vec![0.0f32; b * kd];
1088 let mut kn = vec![0.0f32; b * kd];
1089 for t in 0..b {
1090 for ko in 0..nk {
1091 for (dst, off) in [(&mut qn, 0), (&mut kn, kd)] {
1092 let src = &cq[t * c_dim + off + ko * dk..t * c_dim + off + (ko + 1) * dk];
1093 let ss: f64 = src.iter().map(|&x| x as f64 * x as f64).sum();
1094 let inv = (1.0 / (ss + 1e-6).sqrt()) as f32;
1095 for d in 0..dk {
1096 dst[t * kd + ko * dk + d] = src[d] * inv;
1097 }
1098 }
1099 }
1100 }
1101 let len_log = (b as f64 / 32.0).ln();
1103 let q_scale = 1.0 / (dk as f64).sqrt();
1104
1105 let mut of = vec![0.0f32; b * vd];
1106 let of_ptr = SendMutF32(of.as_mut_ptr());
1107 let (cq, qn, kn, z, a) = (&cq, &qn, &kn, &z, &a);
1108 let heads = |h0: usize, h1: usize| {
1109 let of_ptr = of_ptr;
1110 let mut logits = vec![0f64; b + 1];
1111 let mut o = vec![0f64; dv];
1112 for h in h0..h1 {
1113 let ko = h / rep;
1114 let s = &s_all[h * dk * dv..(h + 1) * dk * dv];
1115 let hist_logit = pw.history_bias[h] as f64 + pw.history_c[h] as f64 * len_log;
1116 let scale = pw.logit_scale[h] as f64;
1117 for t in 0..b {
1118 let q = &qn[t * kd + ko * dk..t * kd + (ko + 1) * dk];
1119 for (sx, lg) in logits[..b].iter_mut().enumerate() {
1120 let k = &kn[sx * kd + ko * dk..sx * kd + (ko + 1) * dk];
1121 let dot: f64 = q.iter().zip(k).map(|(&x, &y)| x as f64 * y as f64).sum();
1122 *lg = dot * scale;
1123 }
1124 logits[b] = hist_logit;
1125 let mx = logits.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1126 let mut sum = 0f64;
1127 for lg in logits.iter_mut() {
1128 *lg = (*lg - mx).exp();
1129 sum += *lg;
1130 }
1131 let g = -(w.a_log[h] as f64).exp() * softplus(a[t * nv + h] as f64 + w.dt_bias[h] as f64);
1133 let w_hist = logits[b] / sum * g.exp() * q_scale;
1134 o.fill(0.0);
1135 for di in 0..dk {
1136 let qd = q[di] as f64 * w_hist;
1137 let row = &s[di * dv..(di + 1) * dv];
1138 for dj in 0..dv {
1139 o[dj] += qd * row[dj] as f64;
1140 }
1141 }
1142 for sx in 0..b {
1143 let ws = logits[sx] / sum;
1144 let v = &cq[sx * c_dim + 2 * kd + h * dv..sx * c_dim + 2 * kd + (h + 1) * dv];
1145 for dj in 0..dv {
1146 o[dj] += ws * v[dj] as f64;
1147 }
1148 }
1149 let ss: f64 = o.iter().map(|&v| v * v).sum();
1151 let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
1152 let oh = unsafe { std::slice::from_raw_parts_mut(of_ptr.0.add(t * vd + h * dv), dv) };
1154 for dj in 0..dv {
1155 let zz = z[t * vd + h * dv + dj] as f64;
1156 let gate = if cfg.output_gate_sigmoid { sigmoid(zz) } else { silu(zz) };
1157 oh[dj] = (o[dj] * inv * w.norm[dj] as f64 * gate) as f32;
1158 }
1159 }
1160 }
1161 };
1162 match pool {
1163 Some(p) => p.run_rows(nv, &heads),
1164 None => heads(0, nv),
1165 }
1166 let mut out = vec![0.0f32; b * cfg.hidden_size];
1167 w.out_proj.matmat(&of, b, &mut out, pool);
1168 out
1169}
1170
1171pub fn perf_report() {
1173 if !perf_enabled() {
1174 return;
1175 }
1176 let fc = PERF_GDN_FORWARD_CALLS.load(Ordering::Relaxed);
1177 let bc = PERF_GDN_BATCH_CALLS.load(Ordering::Relaxed);
1178 let sc = PERF_GDN_STEP_CALLS.load(Ordering::Relaxed);
1179 eprintln!(
1180 "[perf-gdn] forward_calls={} forward_ms={:.3} forward_ms_per_call={:.3} batch_calls={} batch_ms={:.3} step_calls={} step_ms={:.3} step_ms_per_call={:.3}",
1181 fc,
1182 PERF_GDN_FORWARD_NS.load(Ordering::Relaxed) as f64 / 1e6,
1183 PERF_GDN_FORWARD_NS.load(Ordering::Relaxed) as f64 / 1e6 / fc.max(1) as f64,
1184 bc,
1185 PERF_GDN_BATCH_NS.load(Ordering::Relaxed) as f64 / 1e6,
1186 sc,
1187 PERF_GDN_STEP_NS.load(Ordering::Relaxed) as f64 / 1e6,
1188 PERF_GDN_STEP_NS.load(Ordering::Relaxed) as f64 / 1e6 / sc.max(1) as f64,
1189 );
1190}
1191
1192fn gdn_projs_eligible(w: &GdnWeights) -> bool {
1196 if std::env::var("CMF_GPU_GDN")
1202 .map(|v| v == "0")
1203 .unwrap_or(false)
1204 {
1205 return false;
1206 }
1207 w.in_proj_qkv.is_q1()
1208 || w.in_proj_qkv.q4t_parts().is_some()
1209 || w.in_proj_qkv.q4tp_parts().is_some()
1210 || std::env::var("CMF_GPU_GDN")
1211 .map(|v| v == "1")
1212 .unwrap_or(false)
1213}
1214
1215fn gdn_projs_gpu(w: &GdnWeights, x: &[f32], qkv: &mut [f32], z: &mut [f32]) -> bool {
1217 use crate::gpu::matvec_batch;
1218 use crate::qtensor::QTensor;
1219 if !crate::gpu::enabled_here() {
1220 return false;
1221 }
1222 fn part<'a>(
1223 t: &'a QTensor,
1224 x: &[f32],
1225 ) -> Option<(
1226 std::sync::Arc<cortiq_core::CmfModel>,
1227 crate::gpu::BatchJob<'a>,
1228 )> {
1229 use crate::gpu::BatchJob;
1230 use crate::qtensor::prescale;
1231 use cortiq_core::TensorDtype;
1232 match t {
1233 QTensor::Mapped {
1234 model,
1235 idx,
1236 dtype: dt @ (TensorDtype::Q8Row | TensorDtype::Q8_2f),
1237 rows,
1238 cols,
1239 row_scale,
1240 col_field,
1241 ..
1242 } => Some((
1243 model.clone(),
1244 BatchJob {
1245 idx: *idx,
1246 rows: *rows,
1247 cols: *cols,
1248 row_scale,
1249 xs: prescale(x, col_field, *dt).into_owned(),
1250 layout: crate::gpu::BatchLayout::Q8,
1251 },
1252 )),
1253 QTensor::Mapped {
1254 model,
1255 idx,
1256 dtype: TensorDtype::Q1,
1257 rows,
1258 cols,
1259 ..
1260 } => Some((
1261 model.clone(),
1262 BatchJob {
1263 idx: *idx,
1264 rows: *rows,
1265 cols: *cols,
1266 row_scale: &[],
1267 xs: x.to_vec(),
1268 layout: crate::gpu::BatchLayout::Q1,
1269 },
1270 )),
1271 QTensor::Mapped {
1272 model,
1273 idx,
1274 dtype: TensorDtype::Q1G,
1275 rows,
1276 cols,
1277 ..
1278 } => Some((
1279 model.clone(),
1280 BatchJob {
1281 idx: *idx,
1282 rows: *rows,
1283 cols: *cols,
1284 row_scale: &[],
1285 xs: x.to_vec(),
1286 layout: crate::gpu::BatchLayout::Q1g,
1287 },
1288 )),
1289 QTensor::Mapped {
1293 model,
1294 idx,
1295 dtype: dt @ (TensorDtype::Q4Tiled | TensorDtype::Q4TiledP),
1296 rows,
1297 cols,
1298 ..
1299 } => Some((
1300 model.clone(),
1301 BatchJob {
1302 idx: *idx,
1303 rows: *rows,
1304 cols: *cols,
1305 row_scale: &[],
1306 xs: x.to_vec(),
1307 layout: if *dt == TensorDtype::Q4TiledP {
1308 crate::gpu::BatchLayout::Q4tp
1309 } else {
1310 crate::gpu::BatchLayout::Q4t
1311 },
1312 },
1313 )),
1314 _ => None,
1315 }
1316 }
1317 let Some((model, jq)) = part(&w.in_proj_qkv, x) else {
1318 return false;
1319 };
1320 let Some((_, jz)) = part(&w.in_proj_z, x) else {
1321 return false;
1322 };
1323 matvec_batch(&model, &[jq, jz], &mut [qkv, z])
1324}
1325
1326#[allow(clippy::too_many_arguments)]
1329pub fn gdn_pair(
1330 x1: &[f32],
1331 x2: &[f32],
1332 w: &GdnWeights,
1333 cfg: &GdnCfg,
1334 state: &mut Vec<f32>,
1335 scratch: &mut Vec<f32>,
1336 pool: Option<&Pool>,
1337) -> (Vec<f32>, Vec<f32>) {
1338 if state.len() != cfg.state_len() {
1339 *state = vec![0f32; cfg.state_len()];
1340 }
1341 let (c_dim, vd, nv) = (
1342 cfg.conv_dim(),
1343 cfg.num_v_heads * cfg.value_head_dim,
1344 cfg.num_v_heads,
1345 );
1346
1347 let mut qkv1 = vec![0.0f32; c_dim];
1348 let mut qkv2 = vec![0.0f32; c_dim];
1349 w.in_proj_qkv.matvec2(x1, x2, &mut qkv1, &mut qkv2, pool);
1350 let mut z1 = vec![0.0f32; vd];
1351 let mut z2 = vec![0.0f32; vd];
1352 w.in_proj_z.matvec2(x1, x2, &mut z1, &mut z2, pool);
1353 let mut a1 = vec![0.0f32; nv];
1354 let mut a2 = vec![0.0f32; nv];
1355 w.in_proj_a.matvec2(x1, x2, &mut a1, &mut a2, pool);
1356 let mut b1 = vec![0.0f32; nv];
1357 let mut b2 = vec![0.0f32; nv];
1358 w.in_proj_b.matvec2(x1, x2, &mut b1, &mut b2, pool);
1359
1360 let mut of1 = vec![0.0f32; vd];
1361 gdn_step(&qkv1, &z1, &a1, &b1, w, cfg, state, &mut of1, pool);
1362
1363 scratch.clear();
1364 scratch.extend_from_slice(state);
1365 let mut of2 = vec![0.0f32; vd];
1366 gdn_step(&qkv2, &z2, &a2, &b2, w, cfg, scratch, &mut of2, pool);
1367
1368 let mut out1 = vec![0.0f32; cfg.hidden_size];
1369 let mut out2 = vec![0.0f32; cfg.hidden_size];
1370 w.out_proj.matvec2(&of1, &of2, &mut out1, &mut out2, pool);
1371 (out1, out2)
1372}
1373
1374pub struct ShortConvWeights {
1381 pub in_proj: QTensor,
1383 pub conv: Vec<f32>,
1387 pub out_proj: QTensor,
1389}
1390
1391#[derive(Clone, Copy)]
1392pub struct ShortConvCfg {
1393 pub hidden_size: usize,
1394 pub kernel: usize,
1396}
1397
1398impl ShortConvCfg {
1399 pub fn state_len(&self) -> usize {
1401 (self.kernel - 1) * self.hidden_size
1402 }
1403}
1404
1405fn short_conv_step(
1415 bcx: &[f32],
1416 conv: &[f32],
1417 cfg: &ShortConvCfg,
1418 ring_state: &mut [f32],
1419 y: &mut [f32],
1420) {
1421 let (h, k) = (cfg.hidden_size, cfg.kernel);
1422 let ring = k - 1;
1423 let (bg, cg, xg) = (&bcx[0..h], &bcx[h..2 * h], &bcx[2 * h..3 * h]);
1424 for c in 0..h {
1425 let bx = bg[c] * xg[c];
1426 let wc = &conv[c * k..(c + 1) * k];
1427 let mut acc = wc[k - 1] * bx;
1429 let rc = &mut ring_state[c * ring..c * ring + ring];
1430 for s in 0..ring {
1431 acc += wc[k - 2 - s] * rc[s];
1432 }
1433 y[c] = cg[c] * acc;
1434 for s in (1..ring).rev() {
1436 rc[s] = rc[s - 1];
1437 }
1438 if ring > 0 {
1439 rc[0] = bx;
1440 }
1441 }
1442}
1443
1444pub fn short_conv_forward(
1446 x: &[f32],
1447 w: &ShortConvWeights,
1448 cfg: &ShortConvCfg,
1449 state: &mut Vec<f32>,
1450 pool: Option<&Pool>,
1451) -> Vec<f32> {
1452 if state.len() != cfg.state_len() {
1453 *state = vec![0f32; cfg.state_len()];
1454 }
1455 let h = cfg.hidden_size;
1456 let mut bcx = vec![0.0f32; 3 * h];
1457 w.in_proj.matvec(x, &mut bcx, pool);
1458 let mut y = vec![0.0f32; h];
1459 short_conv_step(&bcx, &w.conv, cfg, state, &mut y);
1460 let mut out = vec![0.0f32; h];
1461 w.out_proj.matvec(&y, &mut out, pool);
1462 out
1463}
1464
1465pub fn short_conv_forward_batch(
1470 xs: &[f32],
1471 b: usize,
1472 w: &ShortConvWeights,
1473 cfg: &ShortConvCfg,
1474 state: &mut Vec<f32>,
1475 pool: Option<&Pool>,
1476) -> Vec<f32> {
1477 if state.len() != cfg.state_len() {
1478 *state = vec![0f32; cfg.state_len()];
1479 }
1480 let h = cfg.hidden_size;
1481 let mut bcx = vec![0.0f32; b * 3 * h];
1482 w.in_proj.matmat(xs, b, &mut bcx, pool);
1483 let mut y = vec![0.0f32; b * h];
1484 for bi in 0..b {
1485 short_conv_step(
1486 &bcx[bi * 3 * h..(bi + 1) * 3 * h],
1487 &w.conv,
1488 cfg,
1489 state,
1490 &mut y[bi * h..(bi + 1) * h],
1491 );
1492 }
1493 let mut out = vec![0.0f32; b * h];
1494 w.out_proj.matmat(&y, b, &mut out, pool);
1495 out
1496}
1497
1498#[allow(clippy::too_many_arguments)]
1503pub fn short_conv_pair(
1504 x1: &[f32],
1505 x2: &[f32],
1506 w: &ShortConvWeights,
1507 cfg: &ShortConvCfg,
1508 state: &mut Vec<f32>,
1509 scratch: &mut Vec<f32>,
1510 pool: Option<&Pool>,
1511) -> (Vec<f32>, Vec<f32>) {
1512 if state.len() != cfg.state_len() {
1513 *state = vec![0f32; cfg.state_len()];
1514 }
1515 let h = cfg.hidden_size;
1516 let mut bcx1 = vec![0.0f32; 3 * h];
1517 let mut bcx2 = vec![0.0f32; 3 * h];
1518 w.in_proj.matvec2(x1, x2, &mut bcx1, &mut bcx2, pool);
1519
1520 let mut y1 = vec![0.0f32; h];
1521 short_conv_step(&bcx1, &w.conv, cfg, state, &mut y1);
1522 scratch.clear();
1523 scratch.extend_from_slice(state);
1524 let mut y2 = vec![0.0f32; h];
1525 short_conv_step(&bcx2, &w.conv, cfg, scratch, &mut y2);
1526
1527 let mut out1 = vec![0.0f32; h];
1528 let mut out2 = vec![0.0f32; h];
1529 w.out_proj.matvec2(&y1, &y2, &mut out1, &mut out2, pool);
1530 (out1, out2)
1531}
1532
1533pub struct KdaWeights {
1544 pub q_proj: QTensor,
1546 pub k_proj: QTensor,
1548 pub v_proj: QTensor,
1550 pub conv_q: Vec<f32>,
1552 pub conv_k: Vec<f32>,
1553 pub conv_v: Vec<f32>,
1555 pub f_a: QTensor,
1557 pub f_b: QTensor,
1559 pub dt_bias: Vec<f32>,
1561 pub a_log: Vec<f32>,
1564 pub b_proj: QTensor,
1566 pub gate: KdaOutGate,
1568 pub o_norm: Vec<f32>,
1570 pub o_proj: QTensor,
1572 pub gate_lower_bound: Option<f32>,
1575}
1576
1577pub enum KdaOutGate {
1578 Full(QTensor),
1580 LowRank(QTensor, QTensor),
1582}
1583
1584#[derive(Clone, Copy)]
1585pub struct KdaCfg {
1586 pub num_heads: usize,
1587 pub head_k_dim: usize,
1588 pub head_v_dim: usize,
1589 pub conv_kernel: usize,
1590 pub hidden_size: usize,
1591 pub rms_eps: f64,
1592}
1593
1594impl KdaCfg {
1595 pub fn state_len(&self) -> usize {
1598 let (nh, dk, dv, kk) = (
1599 self.num_heads,
1600 self.head_k_dim,
1601 self.head_v_dim,
1602 self.conv_kernel,
1603 );
1604 (kk - 1) * (2 * nh * dk + nh * dv) + nh * dk * dv
1605 }
1606}
1607
1608fn kda_conv(raw: &[f32], taps: &[f32], ring: &mut [f32], kk: usize, out: &mut [f32]) {
1611 let c_dim = raw.len();
1612 for c in 0..c_dim {
1613 let t = &taps[c * kk..(c + 1) * kk];
1614 let mut acc = raw[c] as f64 * t[kk - 1] as f64;
1615 for j in 0..kk - 1 {
1616 acc += ring[j * c_dim + c] as f64 * t[j] as f64;
1617 }
1618 out[c] = silu(acc) as f32;
1619 }
1620 if kk > 1 {
1621 ring.copy_within(c_dim.., 0);
1622 let tail = (kk - 2) * c_dim;
1623 ring[tail..tail + c_dim].copy_from_slice(raw);
1624 }
1625}
1626
1627#[inline]
1630fn kda_log_decay(w: &KdaWeights, cfg: &KdaCfg, h: usize, d: usize, f: f32) -> f64 {
1631 let (nh, dk) = (cfg.num_heads, cfg.head_k_dim);
1632 let a = if w.a_log.len() == nh {
1633 w.a_log[h] as f64
1634 } else if w.a_log.len() == dk {
1635 w.a_log[d] as f64
1636 } else {
1637 w.a_log[h * dk + d] as f64
1638 };
1639 let raw = f as f64 + w.dt_bias[h * dk + d] as f64;
1640 match w.gate_lower_bound {
1641 Some(lb) => lb as f64 * sigmoid(a.exp() * raw),
1642 None => -a.exp() * softplus(raw),
1643 }
1644}
1645
1646#[allow(clippy::too_many_arguments)]
1654fn kda_step(
1655 xq: &[f32],
1656 xk: &[f32],
1657 xv: &[f32],
1658 f: &[f32],
1659 b: &[f32],
1660 gate_out: &[f32],
1661 w: &KdaWeights,
1662 cfg: &KdaCfg,
1663 state: &mut [f32],
1664 of: &mut [f32],
1665 pool: Option<&Pool>,
1666) {
1667 let (nh, dk, dv, kk) = (
1668 cfg.num_heads,
1669 cfg.head_k_dim,
1670 cfg.head_v_dim,
1671 cfg.conv_kernel,
1672 );
1673 let (kd, vd) = (nh * dk, nh * dv);
1674 let ring_q_len = (kk - 1) * kd;
1675 let ring_v_len = (kk - 1) * vd;
1676 let (ring_q, rest) = state.split_at_mut(ring_q_len);
1677 let (ring_k, rest) = rest.split_at_mut(ring_q_len);
1678 let (ring_v, s_all) = rest.split_at_mut(ring_v_len);
1679
1680 let mut cq = vec![0f32; kd];
1681 let mut ck = vec![0f32; kd];
1682 let mut cv = vec![0f32; vd];
1683 kda_conv(xq, &w.conv_q, ring_q, kk, &mut cq);
1684 kda_conv(xk, &w.conv_k, ring_k, kk, &mut ck);
1685 kda_conv(xv, &w.conv_v, ring_v, kk, &mut cv);
1686
1687 let (cq, ck, cv) = (&cq, &ck, &cv);
1688 let s_ptr = SendMutF32(s_all.as_mut_ptr());
1689 let of_ptr = SendMutF32(of.as_mut_ptr());
1690 let head_range = |h0: usize, h1: usize| {
1691 let (s_ptr, of_ptr) = (s_ptr, of_ptr);
1692 let mut kv = crate::attention::take_buf(dv);
1693 let mut delta = crate::attention::take_buf(dv);
1694 let mut o = crate::attention::take_buf(dv);
1695 let mut kf = crate::attention::take_buf(dk);
1696 let mut qf = crate::attention::take_buf(dk);
1697 let mut gd = crate::attention::take_buf(dk);
1698 for h in h0..h1 {
1699 let qs = h * dk;
1700 let (mut nq, mut nkn) = (0f64, 0f64);
1702 for d in 0..dk {
1703 nq += (cq[qs + d] as f64) * (cq[qs + d] as f64);
1704 nkn += (ck[qs + d] as f64) * (ck[qs + d] as f64);
1705 }
1706 let invq = (1.0 / ((nq + 1e-6).sqrt() * (dk as f64).sqrt())) as f32;
1707 let invk = (1.0 / (nkn + 1e-6).sqrt()) as f32;
1708 for d in 0..dk {
1709 qf[d] = cq[qs + d] * invq;
1710 kf[d] = ck[qs + d] * invk;
1711 gd[d] = kda_log_decay(w, cfg, h, d, f[qs + d]).exp() as f32;
1712 }
1713 let beta = sigmoid(b[h] as f64) as f32;
1714
1715 let s = unsafe { std::slice::from_raw_parts_mut(s_ptr.0.add(h * dk * dv), dk * dv) };
1717 let oh = unsafe { std::slice::from_raw_parts_mut(of_ptr.0.add(h * dv), dv) };
1718 let vt = &cv[h * dv..(h + 1) * dv];
1719
1720 kv[..dv].fill(0.0);
1722 for di in 0..dk {
1723 let kg = kf[di] * gd[di];
1724 let row = &s[di * dv..(di + 1) * dv];
1725 for dj in 0..dv {
1726 kv[dj] += row[dj] * kg;
1727 }
1728 }
1729 for dj in 0..dv {
1730 delta[dj] = (vt[dj] - kv[dj]) * beta;
1731 }
1732 o[..dv].fill(0.0);
1734 for di in 0..dk {
1735 let (kfd, qfd, gdd) = (kf[di], qf[di], gd[di]);
1736 let row = &mut s[di * dv..(di + 1) * dv];
1737 for dj in 0..dv {
1738 let cell = gdd * row[dj] + kfd * delta[dj];
1739 row[dj] = cell;
1740 o[dj] += qfd * cell;
1741 }
1742 }
1743 let ss: f64 = o[..dv].iter().map(|&v| (v as f64) * (v as f64)).sum();
1745 let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
1746 for dj in 0..dv {
1747 oh[dj] = ((o[dj] as f64 * inv)
1748 * w.o_norm[dj] as f64
1749 * sigmoid(gate_out[h * dv + dj] as f64)) as f32;
1750 }
1751 }
1752 crate::attention::recycle_buf(&mut kv);
1753 crate::attention::recycle_buf(&mut delta);
1754 crate::attention::recycle_buf(&mut o);
1755 crate::attention::recycle_buf(&mut kf);
1756 crate::attention::recycle_buf(&mut qf);
1757 crate::attention::recycle_buf(&mut gd);
1758 };
1759 match pool {
1760 Some(pool) if nh >= 4 => pool.run(&|widx, n| {
1761 let chunk = nh.div_ceil(n);
1762 let h0 = (widx * chunk).min(nh);
1763 let h1 = (h0 + chunk).min(nh);
1764 if h0 < h1 {
1765 head_range(h0, h1);
1766 }
1767 }),
1768 _ => head_range(0, nh),
1769 }
1770}
1771
1772fn kda_gate_out(w: &KdaWeights, x: &[f32], vd: usize, pool: Option<&Pool>) -> Vec<f32> {
1775 let mut g = vec![0.0f32; vd];
1776 match &w.gate {
1777 KdaOutGate::Full(gp) => gp.matvec(x, &mut g, pool),
1778 KdaOutGate::LowRank(ga, gb) => {
1779 let mut low = vec![0.0f32; ga.rows()];
1780 ga.matvec(x, &mut low, pool);
1781 gb.matvec(&low, &mut g, pool);
1782 }
1783 }
1784 g
1785}
1786
1787pub fn kda_forward(
1789 x: &[f32],
1790 w: &KdaWeights,
1791 cfg: &KdaCfg,
1792 state: &mut Vec<f32>,
1793 pool: Option<&Pool>,
1794) -> Vec<f32> {
1795 if state.len() != cfg.state_len() {
1796 *state = vec![0f32; cfg.state_len()];
1797 }
1798 let (nh, dk, dv) = (cfg.num_heads, cfg.head_k_dim, cfg.head_v_dim);
1799 let (kd, vd) = (nh * dk, nh * dv);
1800
1801 let mut xq = vec![0.0f32; kd];
1802 let mut xk = vec![0.0f32; kd];
1803 let mut xv = vec![0.0f32; vd];
1804 let mut fl = vec![0.0f32; w.f_a.rows()];
1805 let mut b = vec![0.0f32; nh];
1806 let q4_group = [&w.q_proj, &w.k_proj, &w.v_proj, &w.f_a, &w.b_proj];
1811 let q4_uniform = q4_group.iter().all(|t| {
1812 matches!(
1813 t,
1814 QTensor::Mapped {
1815 dtype: TensorDtype::Q4TiledP,
1816 ..
1817 }
1818 )
1819 });
1820 if q4_uniform {
1821 QTensor::matvec_many(
1822 q4_group,
1823 x,
1824 [
1825 xq.as_mut_slice(),
1826 xk.as_mut_slice(),
1827 xv.as_mut_slice(),
1828 fl.as_mut_slice(),
1829 b.as_mut_slice(),
1830 ],
1831 pool,
1832 );
1833 } else {
1834 QTensor::matvec_many(
1835 [&w.q_proj, &w.k_proj, &w.v_proj, &w.f_a],
1836 x,
1837 [
1838 xq.as_mut_slice(),
1839 xk.as_mut_slice(),
1840 xv.as_mut_slice(),
1841 fl.as_mut_slice(),
1842 ],
1843 pool,
1844 );
1845 w.b_proj.matvec(x, &mut b, pool);
1846 }
1847 let mut f = vec![0.0f32; kd];
1848 w.f_b.matvec(&fl, &mut f, pool);
1849 let gate_out = kda_gate_out(w, x, vd, pool);
1850
1851 let mut of = vec![0.0f32; vd];
1852 kda_step(
1853 &xq, &xk, &xv, &f, &b, &gate_out, w, cfg, state, &mut of, pool,
1854 );
1855
1856 let mut out = vec![0.0f32; cfg.hidden_size];
1857 w.o_proj.matvec(&of, &mut out, pool);
1858 out
1859}
1860
1861pub fn kda_forward_batch(
1865 xs: &[f32],
1866 bsz: usize,
1867 w: &KdaWeights,
1868 cfg: &KdaCfg,
1869 state: &mut Vec<f32>,
1870 pool: Option<&Pool>,
1871) -> Vec<f32> {
1872 if state.len() != cfg.state_len() {
1873 *state = vec![0f32; cfg.state_len()];
1874 }
1875 let (nh, dk, dv, hs) = (
1876 cfg.num_heads,
1877 cfg.head_k_dim,
1878 cfg.head_v_dim,
1879 cfg.hidden_size,
1880 );
1881 let (kd, vd) = (nh * dk, nh * dv);
1882
1883 let mut xq = vec![0.0f32; bsz * kd];
1884 w.q_proj.matmat(xs, bsz, &mut xq, pool);
1885 let mut xk = vec![0.0f32; bsz * kd];
1886 w.k_proj.matmat(xs, bsz, &mut xk, pool);
1887 let mut xv = vec![0.0f32; bsz * vd];
1888 w.v_proj.matmat(xs, bsz, &mut xv, pool);
1889 let rank = w.f_a.rows();
1890 let mut fl = vec![0.0f32; bsz * rank];
1891 w.f_a.matmat(xs, bsz, &mut fl, pool);
1892 let mut f = vec![0.0f32; bsz * kd];
1893 w.f_b.matmat(&fl, bsz, &mut f, pool);
1894 let mut b = vec![0.0f32; bsz * nh];
1895 w.b_proj.matmat(xs, bsz, &mut b, pool);
1896 let mut gate_out = vec![0.0f32; bsz * vd];
1897 match &w.gate {
1898 KdaOutGate::Full(gp) => gp.matmat(xs, bsz, &mut gate_out, pool),
1899 KdaOutGate::LowRank(ga, gb) => {
1900 let mut low = vec![0.0f32; bsz * ga.rows()];
1901 ga.matmat(xs, bsz, &mut low, pool);
1902 gb.matmat(&low, bsz, &mut gate_out, pool);
1903 }
1904 }
1905
1906 let mut of = vec![0.0f32; bsz * vd];
1907 for bi in 0..bsz {
1908 let mut oh = vec![0.0f32; vd];
1909 kda_step(
1910 &xq[bi * kd..(bi + 1) * kd],
1911 &xk[bi * kd..(bi + 1) * kd],
1912 &xv[bi * vd..(bi + 1) * vd],
1913 &f[bi * kd..(bi + 1) * kd],
1914 &b[bi * nh..(bi + 1) * nh],
1915 &gate_out[bi * vd..(bi + 1) * vd],
1916 w,
1917 cfg,
1918 state,
1919 &mut oh,
1920 pool,
1921 );
1922 of[bi * vd..(bi + 1) * vd].copy_from_slice(&oh);
1923 }
1924
1925 let mut out = vec![0.0f32; bsz * hs];
1926 w.o_proj.matmat(&of, bsz, &mut out, pool);
1927 out
1928}
1929
1930#[cfg(test)]
1931mod tests {
1932 #[test]
1933 fn kda_forward_matches_naive_reference() {
1934 let (nh, dk, dv, kk, hs, rank) = (2usize, 4usize, 4usize, 3usize, 6usize, 3usize);
1940 let synth = |rows: usize, cols: usize, salt: usize| -> QTensor {
1941 QTensor::from_f32(
1942 (0..rows * cols)
1943 .map(|i| (((i * 31 + salt * 17) % 101) as f32 / 101.0 - 0.5) * 0.6)
1944 .collect(),
1945 rows,
1946 cols,
1947 )
1948 };
1949 let vecf = |n: usize, salt: usize| -> Vec<f32> {
1950 (0..n)
1951 .map(|i| (((i * 13 + salt * 7) % 89) as f32 / 89.0 - 0.5) * 0.8)
1952 .collect()
1953 };
1954 for (label, a_log, lb) in [
1955 ("per-head standard", vecf(nh, 40), None),
1956 ("per-dim lower-bound", vecf(dk, 41), Some(-5.0f32)),
1957 ] {
1958 let w = KdaWeights {
1959 q_proj: synth(nh * dk, hs, 1),
1960 k_proj: synth(nh * dk, hs, 2),
1961 v_proj: synth(nh * dv, hs, 3),
1962 conv_q: vecf(nh * dk * kk, 4),
1963 conv_k: vecf(nh * dk * kk, 5),
1964 conv_v: vecf(nh * dv * kk, 6),
1965 f_a: synth(rank, hs, 7),
1966 f_b: synth(nh * dk, rank, 8),
1967 dt_bias: vecf(nh * dk, 9),
1968 a_log: a_log.clone(),
1969 b_proj: synth(nh, hs, 10),
1970 gate: KdaOutGate::LowRank(synth(rank, hs, 11), synth(nh * dv, rank, 12)),
1971 o_norm: (0..dv).map(|i| 1.0 + 0.1 * i as f32).collect(),
1972 o_proj: synth(hs, nh * dv, 13),
1973 gate_lower_bound: lb,
1974 };
1975 let cfg = KdaCfg {
1976 num_heads: nh,
1977 head_k_dim: dk,
1978 head_v_dim: dv,
1979 conv_kernel: kk,
1980 hidden_size: hs,
1981 rms_eps: 1e-6,
1982 };
1983 let xs: Vec<Vec<f32>> = (0..6)
1984 .map(|t| {
1985 (0..hs)
1986 .map(|i| ((t * hs + i) as f32 * 0.37).sin() * 0.5)
1987 .collect()
1988 })
1989 .collect();
1990
1991 let mut state = Vec::new();
1993 let got: Vec<Vec<f32>> = xs
1994 .iter()
1995 .map(|x| kda_forward(x, &w, &cfg, &mut state, None))
1996 .collect();
1997
1998 let mv = |t: &QTensor, x: &[f32]| -> Vec<f32> {
2000 let mut o = vec![0.0f32; t.rows()];
2001 t.matvec(x, &mut o, None);
2002 o
2003 };
2004 let mut hist: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = Vec::new(); let mut s_state = vec![0f64; nh * dk * dv];
2006 let mut want: Vec<Vec<f32>> = Vec::new();
2007 for x in &xs {
2008 let (xq, xk, xv) = (mv(&w.q_proj, x), mv(&w.k_proj, x), mv(&w.v_proj, x));
2009 hist.push((xq, xk, xv));
2010 let conv = |sel: fn(&(Vec<f32>, Vec<f32>, Vec<f32>)) -> &Vec<f32>,
2012 taps: &[f32],
2013 n: usize|
2014 -> Vec<f32> {
2015 (0..n)
2016 .map(|c| {
2017 let t = &taps[c * kk..(c + 1) * kk];
2018 let mut acc = 0f64;
2019 for j in 0..kk {
2020 let idx = hist.len() as i64 - (kk as i64 - j as i64);
2021 if idx >= 0 {
2022 acc += sel(&hist[idx as usize])[c] as f64 * t[j] as f64;
2023 }
2024 }
2025 silu(acc)
2026 })
2027 .map(|v| v as f32)
2028 .collect()
2029 };
2030 let cq = conv(|h| &h.0, &w.conv_q, nh * dk);
2031 let ck = conv(|h| &h.1, &w.conv_k, nh * dk);
2032 let cv = conv(|h| &h.2, &w.conv_v, nh * dv);
2033 let f = mv(&w.f_b, &mv(&w.f_a, x));
2034 let bb = mv(&w.b_proj, x);
2035 let gate_out = match &w.gate {
2036 KdaOutGate::LowRank(ga, gb) => mv(gb, &mv(ga, x)),
2037 KdaOutGate::Full(g) => mv(g, x),
2038 };
2039 let mut of = vec![0f32; nh * dv];
2040 for h in 0..nh {
2041 let q: Vec<f64> = {
2043 let sl = &cq[h * dk..(h + 1) * dk];
2044 let n: f64 = sl.iter().map(|&v| (v as f64) * (v as f64)).sum();
2045 let inv = 1.0 / ((n + 1e-6).sqrt() * (dk as f64).sqrt());
2046 sl.iter().map(|&v| v as f64 * inv).collect()
2047 };
2048 let k: Vec<f64> = {
2049 let sl = &ck[h * dk..(h + 1) * dk];
2050 let n: f64 = sl.iter().map(|&v| (v as f64) * (v as f64)).sum();
2051 let inv = 1.0 / (n + 1e-6).sqrt();
2052 sl.iter().map(|&v| v as f64 * inv).collect()
2053 };
2054 let v: Vec<f64> = cv[h * dv..(h + 1) * dv].iter().map(|&v| v as f64).collect();
2055 let g: Vec<f64> = (0..dk)
2057 .map(|d| {
2058 let a = if w.a_log.len() == nh {
2059 w.a_log[h] as f64
2060 } else {
2061 w.a_log[d] as f64
2062 };
2063 let raw = f[h * dk + d] as f64 + w.dt_bias[h * dk + d] as f64;
2064 match w.gate_lower_bound {
2065 Some(lb) => lb as f64 * sigmoid(a.exp() * raw),
2066 None => -a.exp() * softplus(raw),
2067 }
2068 })
2069 .collect();
2070 let beta = sigmoid(bb[h] as f64);
2071 let s = &mut s_state[h * dk * dv..(h + 1) * dk * dv];
2072 for di in 0..dk {
2074 for dj in 0..dv {
2075 s[di * dv + dj] *= g[di].exp();
2076 }
2077 }
2078 let mut kv = vec![0f64; dv];
2080 for di in 0..dk {
2081 for dj in 0..dv {
2082 kv[dj] += k[di] * s[di * dv + dj];
2083 }
2084 }
2085 for di in 0..dk {
2086 for dj in 0..dv {
2087 s[di * dv + dj] += beta * k[di] * (v[dj] - kv[dj]);
2088 }
2089 }
2090 let mut o = vec![0f64; dv];
2091 for di in 0..dk {
2092 for dj in 0..dv {
2093 o[dj] += q[di] * s[di * dv + dj];
2094 }
2095 }
2096 let ss: f64 = o.iter().map(|&v| v * v).sum();
2098 let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
2099 for dj in 0..dv {
2100 of[h * dv + dj] = (o[dj]
2101 * inv
2102 * w.o_norm[dj] as f64
2103 * sigmoid(gate_out[h * dv + dj] as f64))
2104 as f32;
2105 }
2106 }
2107 want.push(mv(&w.o_proj, &of));
2108 }
2109
2110 for (t, (g, e)) in got.iter().zip(&want).enumerate() {
2111 for (i, (a, b)) in g.iter().zip(e.iter()).enumerate() {
2112 assert!((a - b).abs() < 2e-4, "{label}: t={t} i={i}: {a} vs {b}");
2113 }
2114 }
2115 }
2116
2117 let w = KdaWeights {
2119 q_proj: synth(nh * dk, hs, 1),
2120 k_proj: synth(nh * dk, hs, 2),
2121 v_proj: synth(nh * dv, hs, 3),
2122 conv_q: vecf(nh * dk * kk, 4),
2123 conv_k: vecf(nh * dk * kk, 5),
2124 conv_v: vecf(nh * dv * kk, 6),
2125 f_a: synth(rank, hs, 7),
2126 f_b: synth(nh * dk, rank, 8),
2127 dt_bias: vecf(nh * dk, 9),
2128 a_log: vecf(nh, 40),
2129 b_proj: synth(nh, hs, 10),
2130 gate: KdaOutGate::LowRank(synth(rank, hs, 11), synth(nh * dv, rank, 12)),
2131 o_norm: (0..dv).map(|i| 1.0 + 0.1 * i as f32).collect(),
2132 o_proj: synth(hs, nh * dv, 13),
2133 gate_lower_bound: None,
2134 };
2135 let cfg = KdaCfg {
2136 num_heads: nh,
2137 head_k_dim: dk,
2138 head_v_dim: dv,
2139 conv_kernel: kk,
2140 hidden_size: hs,
2141 rms_eps: 1e-6,
2142 };
2143 let xs: Vec<f32> = (0..5 * hs).map(|i| (i as f32 * 0.29).cos() * 0.4).collect();
2144 let mut st1 = Vec::new();
2145 let seq: Vec<f32> = (0..5)
2146 .flat_map(|t| kda_forward(&xs[t * hs..(t + 1) * hs], &w, &cfg, &mut st1, None))
2147 .collect();
2148 let mut st2 = Vec::new();
2149 let bat = kda_forward_batch(&xs, 5, &w, &cfg, &mut st2, None);
2150 for (i, (a, b)) in seq.iter().zip(&bat).enumerate() {
2151 assert!((a - b).abs() < 1e-5, "batch i={i}: {a} vs {b}");
2152 }
2153 assert_eq!(st1, st2, "state must match after the chunk");
2154 }
2155
2156 use super::*;
2157
2158 fn tiny() -> (VmfPhaseWeights, VmfPhaseCfg) {
2159 let cfg = VmfPhaseCfg {
2160 num_heads: 2,
2161 nphase: 3,
2162 value_head_dim: 4,
2163 hidden_size: 8,
2164 phase_mass: 0.0,
2165 };
2166 let synth = |rows: usize, cols: usize, salt: usize| {
2167 QTensor::from_f32(
2168 (0..rows * cols)
2169 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
2170 .collect(),
2171 rows,
2172 cols,
2173 )
2174 };
2175 let w = VmfPhaseWeights {
2176 thq: synth(cfg.num_heads * cfg.nphase, cfg.hidden_size, 1),
2177 thk: synth(cfg.num_heads * cfg.nphase, cfg.hidden_size, 2),
2178 v_proj: synth(cfg.num_heads * cfg.value_head_dim, cfg.hidden_size, 3),
2179 out_proj: synth(cfg.hidden_size, cfg.num_heads * cfg.value_head_dim, 4),
2180 decay: (0..cfg.num_heads * 2 * cfg.nphase)
2181 .map(|i| 0.9 + 0.005 * (i % 10) as f64)
2182 .collect(),
2183 conv: None,
2184 k_gate: None,
2185 phase_delta: false,
2186 };
2187 (w, cfg)
2188 }
2189
2190 #[test]
2191 fn state_persists_and_changes_output() {
2192 let (w, cfg) = tiny();
2193 let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
2194 let mut state = Vec::new();
2195 let o1 = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
2196 let o2 = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
2197 assert!(o1.iter().zip(&o2).any(|(a, b)| (a - b).abs() > 1e-6));
2199 assert_eq!(state.len(), cfg.state_len());
2200 }
2201
2202 #[test]
2203 fn phase_delta_matches_independent_token_oracle_and_ignores_phase_mass() {
2204 let cfg = VmfPhaseCfg {
2209 num_heads: 1,
2210 nphase: 2,
2211 value_head_dim: 1,
2212 hidden_size: 2,
2213 phase_mass: 17.0,
2215 };
2216 let w = VmfPhaseWeights {
2217 thq: QTensor::from_f32(vec![1.0, 0.0, 0.0, 1.0], 2, 2),
2218 thk: QTensor::from_f32(vec![0.0, 1.0, 1.0, 0.0], 2, 2),
2219 v_proj: QTensor::from_f32(vec![1.0, 0.0], 1, 2),
2220 out_proj: QTensor::from_f32(vec![1.0, 0.0], 2, 1),
2221 decay: vec![0.9, 0.8, 0.7, 0.6],
2222 conv: None,
2223 k_gate: None, phase_delta: true,
2225 };
2226 let xs = [[0.3f32, 0.7f32], [-0.2, 0.4], [0.8, -0.6]];
2227 let c = 1.0f64 / 2.0f64.sqrt();
2228 let mut oracle_state = [0.0f64; 4];
2229 let mut state = Vec::new();
2230 for x in xs {
2231 let q = [
2232 c * (x[0] as f64).cos(),
2233 c * (x[1] as f64).cos(),
2234 c * (x[0] as f64).sin(),
2235 c * (x[1] as f64).sin(),
2236 ];
2237 let k = [
2238 c * (x[1] as f64).cos(),
2239 c * (x[0] as f64).cos(),
2240 c * (x[1] as f64).sin(),
2241 c * (x[0] as f64).sin(),
2242 ];
2243 let value = x[0] as f64;
2244 let mut read = 0.0;
2245 for f in 0..4 {
2246 oracle_state[f] *= w.decay[f];
2247 read += k[f] * oracle_state[f];
2248 }
2249 for f in 0..4 {
2250 oracle_state[f] += k[f] * (value - read);
2251 }
2252 let want: f32 = q
2253 .iter()
2254 .zip(oracle_state)
2255 .map(|(qf, sf)| (qf * sf) as f32)
2256 .sum();
2257 let got = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
2258 assert!(
2259 (got[0] - want).abs() < 2e-6,
2260 "oracle {want} vs runtime {}",
2261 got[0]
2262 );
2263 assert!(got[1].abs() < 2e-7);
2264 }
2265 assert_eq!(state.len(), cfg.state_len());
2266 for (got, want) in state.iter().zip(oracle_state) {
2267 assert!((*got as f64 - want).abs() < 2e-6);
2268 }
2269 }
2270
2271 #[test]
2272 fn phase_delta_fused_readout_is_bit_exact_over_long_continuation() {
2273 for (nh, nph, dv) in [(1, 1, 1), (3, 5, 7), (8, 32, 128)] {
2276 let cfg = VmfPhaseCfg {
2277 num_heads: nh,
2278 nphase: nph,
2279 value_head_dim: dv,
2280 hidden_size: nh * dv,
2281 phase_mass: 19.0,
2282 };
2283 let p2 = 2 * nph;
2284 let scale = 1.0f64 / (nph as f64).sqrt();
2285 let mut state: Vec<f32> = (0..cfg.state_len() + 11)
2286 .map(|i| (i as f32 * 0.37).sin())
2287 .collect();
2288 let mut reference = state.clone();
2289 let tail = state[cfg.state_len()..].to_vec();
2290 let decay: Vec<f64> = (0..nh * p2).map(|i| [0.0, 0.8, 0.99, 1.0][i % 4]).collect();
2291 for step in 0..128 {
2292 let q: Vec<f32> = (0..nh * nph)
2293 .map(|i| ((i + step * 3) as f32 * 0.13).sin() * 4.0)
2294 .collect();
2295 let k: Vec<f32> = (0..nh * nph)
2296 .map(|i| ((i + step * 7) as f32 * 0.29).cos() * 4.0)
2297 .collect();
2298 let v: Vec<f32> = (0..nh * dv)
2299 .map(|i| ((i + step) as f32 * 0.43).sin())
2300 .collect();
2301 let gates: Vec<f32> = (0..nh).map(|h| [0.0, 0.3, 1.0][(step + h) % 3]).collect();
2302 let kap = (step % 4 != 0).then_some(gates.as_slice());
2303 let mut out = vec![0.125f32; nh * dv];
2304 let mut want = out.clone();
2305 for h in 0..nh {
2306 let feature = |theta: &[f32], f: usize| {
2307 let angle = theta[h * nph + f % nph] as f64;
2308 scale * if f < nph { angle.cos() } else { angle.sin() }
2309 };
2310 let beta = kap.map_or(1.0f64, |g| g[h] as f64);
2311 let mut read = vec![0.0f64; dv];
2312 for f in 0..p2 {
2313 for d in 0..dv {
2314 let at = (h * p2 + f) * dv + d;
2315 read[d] += feature(&k, f) * (decay[h * p2 + f] * reference[at] as f64);
2316 }
2317 }
2318 for f in 0..p2 {
2319 for d in 0..dv {
2320 let at = (h * p2 + f) * dv + d;
2321 reference[at] = (decay[h * p2 + f] * reference[at] as f64
2322 + beta * feature(&k, f) * (v[h * dv + d] as f64 - read[d]))
2323 as f32;
2324 }
2325 }
2326 for f in 0..p2 {
2327 for d in 0..dv {
2328 want[h * dv + d] +=
2329 (feature(&q, f) * reference[(h * p2 + f) * dv + d] as f64) as f32;
2330 }
2331 }
2332 }
2333 phase_delta_step_f32(&q, &k, &v, &decay, kap, &cfg, &mut state, &mut out);
2334 assert_eq!(out, want, "shape {nh}/{nph}/{dv}, step {step}");
2335 assert_eq!(state, reference, "state at step {step}");
2336 assert_eq!(&state[cfg.state_len()..], tail);
2337 }
2338 }
2339 }
2340
2341 #[test]
2342 fn phase_delta_pair_reset_and_legacy_paths_are_distinct() {
2343 let (mut delta, mut cfg) = tiny();
2344 delta.phase_delta = true;
2345 let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
2348 let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
2349 let mut seq_state = Vec::new();
2350 let d1 = vmf_phase_forward(&x1, &delta, &cfg, &mut seq_state, None);
2351 let d2 = vmf_phase_forward(&x2, &delta, &cfg, &mut seq_state, None);
2352
2353 let mut pair_state = Vec::new();
2354 let mut scratch = Vec::new();
2355 let (p1, p2) = vmf_phase_pair(&x1, &x2, &delta, &cfg, &mut pair_state, &mut scratch, None);
2356 assert_eq!(d1, p1);
2357 assert_eq!(d2, p2);
2358 std::mem::swap(&mut pair_state, &mut scratch);
2359 assert_eq!(seq_state, pair_state);
2360
2361 let mut reset = seq_state;
2364 reset.clear();
2365 let after_reset = vmf_phase_forward(&x1, &delta, &cfg, &mut reset, None);
2366 assert_eq!(after_reset, d1);
2367
2368 cfg.phase_mass = 0.0;
2369 let mut legacy = delta;
2370 legacy.phase_delta = false;
2371 let mut legacy_state = Vec::new();
2372 let old = vmf_phase_forward(&x1, &legacy, &cfg, &mut legacy_state, None);
2373 assert!(
2374 old.iter().zip(&d1).any(|(a, b)| (a - b).abs() > 1e-5),
2375 "legacy additive and normalized Phase-Delta paths must differ"
2376 );
2377 }
2378
2379 #[test]
2383 fn phase_mass_zero_is_noop_and_positive_shifts() {
2384 let (w, cfg0) = tiny();
2385 let mut cfg_m = cfg0;
2386 cfg_m.phase_mass = 1.0;
2387 let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.4).sin()).collect();
2388
2389 let mut s0 = Vec::new();
2390 let base = vmf_phase_forward(&x, &w, &cfg0, &mut s0, None);
2391 let mut s0b = Vec::new();
2393 let base2 = vmf_phase_forward(&x, &w, &cfg0, &mut s0b, None);
2394 assert_eq!(base, base2, "mass=0 must be deterministic/no-op");
2395 let mut sm = Vec::new();
2397 let massed = vmf_phase_forward(&x, &w, &cfg_m, &mut sm, None);
2398 assert!(
2399 base.iter().zip(&massed).any(|(a, b)| (a - b).abs() > 1e-5),
2400 "mass>0 must change the output"
2401 );
2402 assert!(massed.iter().all(|v| v.is_finite()));
2403 }
2404
2405 #[test]
2410 fn kappa_gate_open_matches_none_and_closed_writes_nothing() {
2411 let (mut w, cfg) = tiny();
2412 let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
2413
2414 let mut s_none = Vec::new();
2415 let base1 = vmf_phase_forward(&x, &w, &cfg, &mut s_none, None);
2416 let base2 = vmf_phase_forward(&x, &w, &cfg, &mut s_none, None);
2417
2418 w.k_gate = Some((
2420 QTensor::from_f32(
2421 vec![0.0; cfg.num_heads * cfg.hidden_size],
2422 cfg.num_heads,
2423 cfg.hidden_size,
2424 ),
2425 vec![20.0; cfg.num_heads],
2426 ));
2427 let mut s_open = Vec::new();
2428 let o1 = vmf_phase_forward(&x, &w, &cfg, &mut s_open, None);
2429 let o2 = vmf_phase_forward(&x, &w, &cfg, &mut s_open, None);
2430 for (a, b) in base1.iter().zip(&o1).chain(base2.iter().zip(&o2)) {
2431 assert!(
2432 (a - b).abs() < 1e-5,
2433 "open κ must match gateless: {a} vs {b}"
2434 );
2435 }
2436
2437 w.k_gate = Some((
2439 QTensor::from_f32(
2440 vec![0.0; cfg.num_heads * cfg.hidden_size],
2441 cfg.num_heads,
2442 cfg.hidden_size,
2443 ),
2444 vec![-20.0; cfg.num_heads],
2445 ));
2446 let mut s_closed = Vec::new();
2447 let oc = vmf_phase_forward(&x, &w, &cfg, &mut s_closed, None);
2448 assert!(
2449 s_closed.iter().all(|&v| v.abs() < 1e-7),
2450 "closed κ: state must stay empty"
2451 );
2452 assert!(
2453 oc.iter().all(|&v| v.abs() < 1e-6),
2454 "closed κ: empty-state readout"
2455 );
2456 }
2457
2458 #[test]
2459 fn pair_matches_two_singles_bitexact() {
2460 let (w, cfg) = tiny();
2461 let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
2462 let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
2463
2464 let mut s_ref = Vec::new();
2466 let r1 = vmf_phase_forward(&x1, &w, &cfg, &mut s_ref, None);
2467 let r2 = vmf_phase_forward(&x2, &w, &cfg, &mut s_ref, None);
2468
2469 let mut s = Vec::new();
2471 let mut scratch = Vec::new();
2472 let (p1, p2) = vmf_phase_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
2473 assert_eq!(r1, p1, "lane 1 must be bit-identical");
2474 assert_eq!(r2, p2, "lane 2 must be bit-identical");
2475 std::mem::swap(&mut s, &mut scratch);
2477 assert_eq!(s, s_ref, "accepted state must equal sequential state");
2478 }
2479
2480 #[test]
2481 fn rejected_draft_leaves_state_at_lane1() {
2482 let (w, cfg) = tiny();
2483 let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.7).sin()).collect();
2484 let x2 = vec![0.5f32; 8];
2485
2486 let mut s_ref = Vec::new();
2487 let _ = vmf_phase_forward(&x1, &w, &cfg, &mut s_ref, None);
2488
2489 let mut s = Vec::new();
2490 let mut scratch = Vec::new();
2491 let _ = vmf_phase_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
2492 assert_eq!(s, s_ref);
2494 }
2495
2496 fn tiny_gdn() -> (GdnWeights, GdnCfg) {
2499 let cfg = GdnCfg {
2500 num_v_heads: 4,
2501 num_k_heads: 2,
2502 key_head_dim: 3,
2503 value_head_dim: 5,
2504 conv_kernel: 4,
2505 hidden_size: 8,
2506 rms_eps: 1e-6,
2507 output_gate_sigmoid: false,
2508 };
2509 let c_dim = cfg.conv_dim();
2510 let vd = cfg.num_v_heads * cfg.value_head_dim;
2511 let synth = |rows: usize, cols: usize, salt: usize| {
2512 QTensor::from_f32(
2513 (0..rows * cols)
2514 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
2515 .collect(),
2516 rows,
2517 cols,
2518 )
2519 };
2520 let vecf = |n: usize, salt: usize| -> Vec<f32> {
2521 (0..n)
2522 .map(|i| (((i * 11 + salt * 5) % 89) as f32 / 89.0 - 0.5) * 0.6)
2523 .collect()
2524 };
2525 let w = GdnWeights {
2526 in_proj_qkv: synth(c_dim, cfg.hidden_size, 1),
2527 in_proj_z: synth(vd, cfg.hidden_size, 2),
2528 in_proj_a: synth(cfg.num_v_heads, cfg.hidden_size, 3),
2529 in_proj_b: synth(cfg.num_v_heads, cfg.hidden_size, 4),
2530 conv1d: vecf(c_dim * cfg.conv_kernel, 5),
2531 a_log: (0..cfg.num_v_heads).map(|i| 0.2 + 0.3 * i as f32).collect(),
2532 dt_bias: vecf(cfg.num_v_heads, 6),
2533 norm: vec![1.0; cfg.value_head_dim],
2534 out_proj: synth(cfg.hidden_size, vd, 7),
2535 };
2536 (w, cfg)
2537 }
2538
2539 #[test]
2543 fn gdn_scan_matches_steps_bitexact() {
2544 for sigmoid_gate in [false, true] {
2545 let (w, mut cfg) = tiny_gdn();
2546 cfg.output_gate_sigmoid = sigmoid_gate;
2547 let (c_dim, vd, nv) = (cfg.conv_dim(), cfg.num_v_heads * cfg.value_head_dim, cfg.num_v_heads);
2548 let row = |n: usize, t: usize, salt: usize| -> Vec<f32> {
2549 (0..n).map(|i| (((i * 7 + t * 31 + salt * 3) % 83) as f32 / 83.0 - 0.5) * 1.3).collect()
2550 };
2551 let mut s_ref = vec![0f32; cfg.state_len()];
2552 let mut s_scan = s_ref.clone();
2553 for (chunk, b) in [(0usize, 2usize), (1, 7), (2, 1), (3, 5)] {
2554 let qkv: Vec<f32> = (0..b).flat_map(|t| row(c_dim, t + 10 * chunk, 1)).collect();
2555 let z: Vec<f32> = (0..b).flat_map(|t| row(vd, t + 10 * chunk, 2)).collect();
2556 let a: Vec<f32> = (0..b).flat_map(|t| row(nv, t + 10 * chunk, 3)).collect();
2557 let bb: Vec<f32> = (0..b).flat_map(|t| row(nv, t + 10 * chunk, 4)).collect();
2558 let mut of_ref = vec![0f32; b * vd];
2559 for t in 0..b {
2560 gdn_step(
2561 &qkv[t * c_dim..(t + 1) * c_dim],
2562 &z[t * vd..(t + 1) * vd],
2563 &a[t * nv..(t + 1) * nv],
2564 &bb[t * nv..(t + 1) * nv],
2565 &w,
2566 &cfg,
2567 &mut s_ref,
2568 &mut of_ref[t * vd..(t + 1) * vd],
2569 None,
2570 );
2571 }
2572 let mut of_scan = vec![0f32; b * vd];
2573 gdn_scan_batch(&qkv, &z, &a, &bb, b, &w, &cfg, &mut s_scan, &mut of_scan, None);
2574 assert_eq!(of_scan, of_ref, "outputs, chunk {chunk} (b = {b})");
2575 assert_eq!(s_scan, s_ref, "state, chunk {chunk} (b = {b})");
2576 }
2577 }
2578 }
2579
2580 #[test]
2581 fn gdn_state_persists_and_changes_output() {
2582 let (w, cfg) = tiny_gdn();
2583 let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
2584 let mut state = Vec::new();
2585 let o1 = gdn_forward(&x, &w, &cfg, &mut state, None);
2586 let o2 = gdn_forward(&x, &w, &cfg, &mut state, None);
2587 assert!(o1.iter().zip(&o2).any(|(a, b)| (a - b).abs() > 1e-6));
2588 assert_eq!(state.len(), cfg.state_len());
2589 }
2590
2591 #[test]
2592 fn gdn_pair_matches_two_singles_bitexact() {
2593 let (w, cfg) = tiny_gdn();
2594 let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
2595 let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
2596
2597 let mut s_ref = Vec::new();
2598 let r1 = gdn_forward(&x1, &w, &cfg, &mut s_ref, None);
2599 let r2 = gdn_forward(&x2, &w, &cfg, &mut s_ref, None);
2600
2601 let mut s = Vec::new();
2602 let mut scratch = Vec::new();
2603 let (p1, p2) = gdn_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
2604 assert_eq!(r1, p1, "lane 1 must be bit-identical");
2605 assert_eq!(r2, p2, "lane 2 must be bit-identical");
2606 std::mem::swap(&mut s, &mut scratch);
2607 assert_eq!(s, s_ref, "accepted state must equal sequential state");
2608 }
2609
2610 #[test]
2611 fn gdn_rejected_draft_leaves_state_at_lane1() {
2612 let (w, cfg) = tiny_gdn();
2613 let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.7).sin()).collect();
2614 let x2 = vec![0.5f32; 8];
2615
2616 let mut s_ref = Vec::new();
2617 let _ = gdn_forward(&x1, &w, &cfg, &mut s_ref, None);
2618
2619 let mut s = Vec::new();
2620 let mut scratch = Vec::new();
2621 let _ = gdn_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
2622 assert_eq!(s, s_ref);
2623 }
2624
2625 #[test]
2629 fn gdn_conv_ring_matches_explicit_causal_conv() {
2630 let (w, cfg) = tiny_gdn();
2631 let seq: Vec<Vec<f32>> = (0..6)
2632 .map(|t| (0..8).map(|i| ((t * 8 + i) as f32 * 0.17).sin()).collect())
2633 .collect();
2634
2635 let mut s_inc = Vec::new();
2638 for (t, x) in seq.iter().enumerate() {
2639 let inc = gdn_forward(x, &w, &cfg, &mut s_inc, None);
2640 let mut s_replay = Vec::new();
2641 let mut replay = Vec::new();
2642 for xr in &seq[..=t] {
2643 replay = gdn_forward(xr, &w, &cfg, &mut s_replay, None);
2644 }
2645 assert_eq!(inc, replay, "position {t}: ring must equal replay");
2646 }
2647 }
2648
2649 fn tiny_short_conv() -> (ShortConvWeights, ShortConvCfg) {
2650 let cfg = ShortConvCfg {
2651 hidden_size: 8,
2652 kernel: 3,
2653 };
2654 let synth = |rows: usize, cols: usize, salt: usize| {
2655 QTensor::from_f32(
2656 (0..rows * cols)
2657 .map(|i| (((i * 11 + salt * 5) % 89) as f32 / 89.0 - 0.5) * 0.5)
2658 .collect(),
2659 rows,
2660 cols,
2661 )
2662 };
2663 let w = ShortConvWeights {
2664 in_proj: synth(3 * cfg.hidden_size, cfg.hidden_size, 1),
2665 conv: (0..cfg.hidden_size * cfg.kernel)
2666 .map(|i| ((i * 7 % 13) as f32 / 13.0 - 0.5) * 0.8)
2667 .collect(),
2668 out_proj: synth(cfg.hidden_size, cfg.hidden_size, 2),
2669 };
2670 (w, cfg)
2671 }
2672
2673 #[test]
2676 fn short_conv_ring_matches_explicit_causal_conv() {
2677 let (w, cfg) = tiny_short_conv();
2678 let seq: Vec<Vec<f32>> = (0..6)
2679 .map(|t| (0..8).map(|i| ((t * 8 + i) as f32 * 0.19).cos()).collect())
2680 .collect();
2681 let mut s_inc = Vec::new();
2682 for (t, x) in seq.iter().enumerate() {
2683 let inc = short_conv_forward(x, &w, &cfg, &mut s_inc, None);
2684 let mut s_replay = Vec::new();
2685 let mut replay = Vec::new();
2686 for xr in &seq[..=t] {
2687 replay = short_conv_forward(xr, &w, &cfg, &mut s_replay, None);
2688 }
2689 assert_eq!(inc, replay, "position {t}: ring must equal replay");
2690 assert_eq!(s_inc.len(), cfg.state_len());
2691 }
2692 }
2693
2694 #[test]
2697 fn short_conv_batch_matches_sequential() {
2698 let (w, cfg) = tiny_short_conv();
2699 let b = 5;
2700 let xs: Vec<f32> = (0..b * cfg.hidden_size)
2701 .map(|i| (i as f32 * 0.13).sin() * 0.6)
2702 .collect();
2703
2704 let mut s_seq = Vec::new();
2705 let mut seq_out = vec![0.0f32; b * cfg.hidden_size];
2706 for bi in 0..b {
2707 let o = short_conv_forward(
2708 &xs[bi * cfg.hidden_size..(bi + 1) * cfg.hidden_size],
2709 &w,
2710 &cfg,
2711 &mut s_seq,
2712 None,
2713 );
2714 seq_out[bi * cfg.hidden_size..(bi + 1) * cfg.hidden_size].copy_from_slice(&o);
2715 }
2716
2717 let mut s_batch = Vec::new();
2718 let batch_out = short_conv_forward_batch(&xs, b, &w, &cfg, &mut s_batch, None);
2719 assert_eq!(
2720 seq_out, batch_out,
2721 "batch conv must match sequential decode"
2722 );
2723 assert_eq!(s_seq, s_batch, "ring state must match after the chunk");
2724 }
2725}