1use crate::pool::Pool;
27use crate::qtensor::QTensor;
28
29pub struct VmfPhaseWeights {
31 pub thq: QTensor,
33 pub thk: QTensor,
35 pub v_proj: QTensor,
37 pub out_proj: QTensor,
39 pub decay: Vec<f64>,
41}
42
43#[derive(Clone, Copy)]
44pub struct VmfPhaseCfg {
45 pub num_heads: usize,
46 pub nphase: usize,
47 pub value_head_dim: usize,
48 pub hidden_size: usize,
49 pub phase_mass: f32,
58}
59
60impl VmfPhaseCfg {
61 pub fn state_len(&self) -> usize {
62 self.num_heads * 2 * self.nphase * self.value_head_dim
63 }
64}
65
66fn phase_step(
69 thq: &[f32],
70 thk: &[f32],
71 v: &[f32],
72 decay: &[f64],
73 cfg: &VmfPhaseCfg,
74 state: &mut [f64],
75 out: &mut [f32],
76) {
77 let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
78 let mscale = 1.0f64 / (1.0 + cfg.phase_mass as f64);
80 let p2 = 2 * nph;
81 for h in 0..nh {
82 let s = &mut state[h * p2 * dv..(h + 1) * p2 * dv];
83 let thk_h = &thk[h * nph..(h + 1) * nph];
84 let thq_h = &thq[h * nph..(h + 1) * nph];
85 let vt = &v[h * dv..(h + 1) * dv];
86 let ot = &mut out[h * dv..(h + 1) * dv];
87 let dec = &decay[h * p2..(h + 1) * p2];
88 for f in 0..p2 {
89 let (fk, fq) = if f < nph {
91 ((thk_h[f] as f64 * mscale).cos(), (thq_h[f] as f64 * mscale).cos())
92 } else {
93 (
94 (thk_h[f - nph] as f64 * mscale).sin(),
95 (thq_h[f - nph] as f64 * mscale).sin(),
96 )
97 };
98 let row = &mut s[f * dv..(f + 1) * dv];
99 let dcf = dec[f];
100 for d in 0..dv {
101 row[d] = dcf * row[d] + fk * vt[d] as f64; ot[d] += (fq * row[d]) as f32; }
104 }
105 }
106}
107
108pub fn vmf_phase_forward(
110 x: &[f32],
111 w: &VmfPhaseWeights,
112 cfg: &VmfPhaseCfg,
113 state: &mut Vec<f64>,
114 pool: Option<&Pool>,
115) -> Vec<f32> {
116 if state.len() != cfg.state_len() {
117 *state = vec![0f64; cfg.state_len()];
118 }
119 let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
120
121 let mut thq = vec![0.0f32; nh * nph];
122 w.thq.matvec(x, &mut thq, pool);
123 let mut thk = vec![0.0f32; nh * nph];
124 w.thk.matvec(x, &mut thk, pool);
125 let mut v = vec![0.0f32; nh * dv];
126 w.v_proj.matvec(x, &mut v, pool);
127
128 let mut o = vec![0.0f32; nh * dv];
129 phase_step(&thq, &thk, &v, &w.decay, cfg, state, &mut o);
130
131 let mut out = vec![0.0f32; cfg.hidden_size];
132 w.out_proj.matvec(&o, &mut out, pool);
133 out
134}
135
136#[allow(clippy::too_many_arguments)]
141pub fn vmf_phase_pair(
142 x1: &[f32],
143 x2: &[f32],
144 w: &VmfPhaseWeights,
145 cfg: &VmfPhaseCfg,
146 state: &mut Vec<f64>,
147 scratch: &mut Vec<f64>,
148 pool: Option<&Pool>,
149) -> (Vec<f32>, Vec<f32>) {
150 if state.len() != cfg.state_len() {
151 *state = vec![0f64; cfg.state_len()];
152 }
153 let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
154
155 let mut thq1 = vec![0.0f32; nh * nph];
156 let mut thq2 = vec![0.0f32; nh * nph];
157 w.thq.matvec2(x1, x2, &mut thq1, &mut thq2, pool);
158 let mut thk1 = vec![0.0f32; nh * nph];
159 let mut thk2 = vec![0.0f32; nh * nph];
160 w.thk.matvec2(x1, x2, &mut thk1, &mut thk2, pool);
161 let mut v1 = vec![0.0f32; nh * dv];
162 let mut v2 = vec![0.0f32; nh * dv];
163 w.v_proj.matvec2(x1, x2, &mut v1, &mut v2, pool);
164
165 let mut o1 = vec![0.0f32; nh * dv];
167 phase_step(&thq1, &thk1, &v1, &w.decay, cfg, state, &mut o1);
168
169 scratch.clear();
171 scratch.extend_from_slice(state);
172 let mut o2 = vec![0.0f32; nh * dv];
173 phase_step(&thq2, &thk2, &v2, &w.decay, cfg, scratch, &mut o2);
174
175 let mut out1 = vec![0.0f32; cfg.hidden_size];
176 let mut out2 = vec![0.0f32; cfg.hidden_size];
177 w.out_proj.matvec2(&o1, &o2, &mut out1, &mut out2, pool);
178 (out1, out2)
179}
180
181pub struct GdnWeights {
186 pub in_proj_qkv: QTensor,
188 pub in_proj_z: QTensor,
190 pub in_proj_a: QTensor,
192 pub in_proj_b: QTensor,
194 pub conv1d: Vec<f32>,
196 pub a_log: Vec<f32>,
198 pub dt_bias: Vec<f32>,
200 pub norm: Vec<f32>,
202 pub out_proj: QTensor,
204}
205
206#[derive(Clone, Copy)]
207pub struct GdnCfg {
208 pub num_v_heads: usize,
209 pub num_k_heads: usize,
210 pub key_head_dim: usize,
211 pub value_head_dim: usize,
212 pub conv_kernel: usize,
213 pub hidden_size: usize,
214 pub rms_eps: f64,
215}
216
217impl GdnCfg {
218 pub fn conv_dim(&self) -> usize {
219 2 * self.num_k_heads * self.key_head_dim + self.num_v_heads * self.value_head_dim
220 }
221
222 pub fn state_len(&self) -> usize {
225 (self.conv_kernel - 1) * self.conv_dim()
226 + self.num_v_heads * self.key_head_dim * self.value_head_dim
227 }
228}
229
230fn softplus(x: f64) -> f64 {
231 if x > 20.0 {
232 x
233 } else {
234 x.exp().ln_1p()
235 }
236}
237
238fn sigmoid(x: f64) -> f64 {
239 1.0 / (1.0 + (-x).exp())
240}
241
242fn silu(x: f64) -> f64 {
243 x / (1.0 + (-x).exp())
244}
245
246#[allow(clippy::too_many_arguments)]
250fn gdn_step(qkv: &[f32], z: &[f32], a: &[f32], b: &[f32], w: &GdnWeights, cfg: &GdnCfg, state: &mut [f64], of: &mut [f32]) {
251 let (nv, nk, dk, dv, kk) = (
252 cfg.num_v_heads,
253 cfg.num_k_heads,
254 cfg.key_head_dim,
255 cfg.value_head_dim,
256 cfg.conv_kernel,
257 );
258 let c_dim = cfg.conv_dim();
259 let (kd, rep) = (nk * dk, nv / nk);
260 let (ring, s_all) = state.split_at_mut((kk - 1) * c_dim);
261
262 let mut cq = vec![0f64; c_dim];
265 for c in 0..c_dim {
266 let taps = &w.conv1d[c * kk..(c + 1) * kk];
267 let mut acc = qkv[c] as f64 * taps[kk - 1] as f64;
268 for j in 0..kk - 1 {
269 acc += ring[j * c_dim + c] * taps[j] as f64;
270 }
271 cq[c] = silu(acc);
272 }
273 if kk > 1 {
275 ring.copy_within(c_dim.., 0);
276 let tail = (kk - 2) * c_dim;
277 for c in 0..c_dim {
278 ring[tail + c] = qkv[c] as f64;
279 }
280 }
281
282 for h in 0..nv {
283 let ko = h / rep; let (qs, ks) = (ko * dk, kd + ko * dk);
285 let (mut nq, mut nkn) = (0f64, 0f64);
287 for d in 0..dk {
288 nq += cq[qs + d] * cq[qs + d];
289 nkn += cq[ks + d] * cq[ks + d];
290 }
291 let invq = 1.0 / ((nq + 1e-6).sqrt() * (dk as f64).sqrt());
292 let invk = 1.0 / (nkn + 1e-6).sqrt();
293
294 let g = (-(w.a_log[h] as f64).exp() * softplus(a[h] as f64 + w.dt_bias[h] as f64)).exp();
295 let beta = sigmoid(b[h] as f64);
296
297 let s = &mut s_all[h * dk * dv..(h + 1) * dk * dv];
298 let vt = &cq[2 * kd + h * dv..2 * kd + (h + 1) * dv];
299 let mut kv = vec![0f64; dv];
301 for di in 0..dk {
302 let kf = cq[ks + di] * invk;
303 let row = &mut s[di * dv..(di + 1) * dv];
304 for dj in 0..dv {
305 row[dj] *= g;
306 kv[dj] += row[dj] * kf;
307 }
308 }
309 let mut o = vec![0f64; dv];
310 for di in 0..dk {
311 let kf = cq[ks + di] * invk;
312 let qf = cq[qs + di] * invq;
313 let row = &mut s[di * dv..(di + 1) * dv];
314 for dj in 0..dv {
315 row[dj] += kf * (vt[dj] - kv[dj]) * beta;
316 o[dj] += qf * row[dj];
317 }
318 }
319 let ss: f64 = o.iter().map(|v| v * v).sum();
321 let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
322 for dj in 0..dv {
323 of[h * dv + dj] =
324 ((o[dj] * inv) * w.norm[dj] as f64 * silu(z[h * dv + dj] as f64)) as f32;
325 }
326 }
327}
328
329pub fn gdn_forward(
331 x: &[f32],
332 w: &GdnWeights,
333 cfg: &GdnCfg,
334 state: &mut Vec<f64>,
335 pool: Option<&Pool>,
336) -> Vec<f32> {
337 if state.len() != cfg.state_len() {
338 *state = vec![0f64; cfg.state_len()];
339 }
340 let (c_dim, vd) = (cfg.conv_dim(), cfg.num_v_heads * cfg.value_head_dim);
341
342 let mut qkv = vec![0.0f32; c_dim];
343 let mut z = vec![0.0f32; vd];
344 if !gdn_projs_gpu(w, x, &mut qkv, &mut z) {
348 w.in_proj_qkv.matvec(x, &mut qkv, pool);
349 w.in_proj_z.matvec(x, &mut z, pool);
350 }
351 let mut a = vec![0.0f32; cfg.num_v_heads];
352 w.in_proj_a.matvec(x, &mut a, pool);
353 let mut b = vec![0.0f32; cfg.num_v_heads];
354 w.in_proj_b.matvec(x, &mut b, pool);
355
356 let mut of = vec![0.0f32; vd];
357 gdn_step(&qkv, &z, &a, &b, w, cfg, state, &mut of);
358
359 let mut out = vec![0.0f32; cfg.hidden_size];
360 w.out_proj.matvec(&of, &mut out, pool);
361 out
362}
363
364pub fn gdn_forward_batch(
369 xs: &[f32],
370 b: usize,
371 w: &GdnWeights,
372 cfg: &GdnCfg,
373 state: &mut Vec<f64>,
374 pool: Option<&Pool>,
375) -> Vec<f32> {
376 if state.len() != cfg.state_len() {
377 *state = vec![0f64; cfg.state_len()];
378 }
379 let (c_dim, vd) = (cfg.conv_dim(), cfg.num_v_heads * cfg.value_head_dim);
380 let nv = cfg.num_v_heads;
381
382 let mut qkv = vec![0.0f32; b * c_dim];
383 w.in_proj_qkv.matmat(xs, b, &mut qkv, pool);
384 let mut z = vec![0.0f32; b * vd];
385 w.in_proj_z.matmat(xs, b, &mut z, pool);
386 let mut a = vec![0.0f32; b * nv];
387 w.in_proj_a.matmat(xs, b, &mut a, pool);
388 let mut bb = vec![0.0f32; b * nv];
389 w.in_proj_b.matmat(xs, b, &mut bb, pool);
390
391 let mut of = vec![0.0f32; b * vd];
392 for bi in 0..b {
393 gdn_step(
394 &qkv[bi * c_dim..(bi + 1) * c_dim],
395 &z[bi * vd..(bi + 1) * vd],
396 &a[bi * nv..(bi + 1) * nv],
397 &bb[bi * nv..(bi + 1) * nv],
398 w,
399 cfg,
400 state,
401 &mut of[bi * vd..(bi + 1) * vd],
402 );
403 }
404 let mut out = vec![0.0f32; b * cfg.hidden_size];
405 w.out_proj.matmat(&of, b, &mut out, pool);
406 out
407}
408
409fn gdn_projs_gpu(w: &GdnWeights, x: &[f32], qkv: &mut [f32], z: &mut [f32]) -> bool {
411 use crate::gpu::matvec_batch;
412 use crate::qtensor::QTensor;
413 if !crate::gpu::enabled_here()
417 || !std::env::var("CMF_GPU_GDN").map(|v| v == "1").unwrap_or(false)
418 {
419 return false;
420 }
421 fn part<'a>(
422 t: &'a QTensor,
423 x: &[f32],
424 ) -> Option<(std::sync::Arc<cortiq_core::CmfModel>, crate::gpu::BatchJob<'a>)> {
425 use crate::gpu::BatchJob;
426 use crate::qtensor::prescale;
427 use cortiq_core::TensorDtype;
428 match t {
429 QTensor::Mapped {
430 model,
431 idx,
432 dtype: dt @ (TensorDtype::Q8Row | TensorDtype::Q8_2f),
433 rows,
434 cols,
435 row_scale,
436 col_field,
437 } => Some((
438 model.clone(),
439 BatchJob {
440 idx: *idx,
441 rows: *rows,
442 cols: *cols,
443 row_scale,
444 xs: prescale(x, col_field, *dt),
445 },
446 )),
447 _ => None,
448 }
449 }
450 let Some((model, jq)) = part(&w.in_proj_qkv, x) else { return false };
451 let Some((_, jz)) = part(&w.in_proj_z, x) else { return false };
452 matvec_batch(&model, &[jq, jz], &mut [qkv, z])
453}
454
455#[allow(clippy::too_many_arguments)]
458pub fn gdn_pair(
459 x1: &[f32],
460 x2: &[f32],
461 w: &GdnWeights,
462 cfg: &GdnCfg,
463 state: &mut Vec<f64>,
464 scratch: &mut Vec<f64>,
465 pool: Option<&Pool>,
466) -> (Vec<f32>, Vec<f32>) {
467 if state.len() != cfg.state_len() {
468 *state = vec![0f64; cfg.state_len()];
469 }
470 let (c_dim, vd, nv) = (
471 cfg.conv_dim(),
472 cfg.num_v_heads * cfg.value_head_dim,
473 cfg.num_v_heads,
474 );
475
476 let mut qkv1 = vec![0.0f32; c_dim];
477 let mut qkv2 = vec![0.0f32; c_dim];
478 w.in_proj_qkv.matvec2(x1, x2, &mut qkv1, &mut qkv2, pool);
479 let mut z1 = vec![0.0f32; vd];
480 let mut z2 = vec![0.0f32; vd];
481 w.in_proj_z.matvec2(x1, x2, &mut z1, &mut z2, pool);
482 let mut a1 = vec![0.0f32; nv];
483 let mut a2 = vec![0.0f32; nv];
484 w.in_proj_a.matvec2(x1, x2, &mut a1, &mut a2, pool);
485 let mut b1 = vec![0.0f32; nv];
486 let mut b2 = vec![0.0f32; nv];
487 w.in_proj_b.matvec2(x1, x2, &mut b1, &mut b2, pool);
488
489 let mut of1 = vec![0.0f32; vd];
490 gdn_step(&qkv1, &z1, &a1, &b1, w, cfg, state, &mut of1);
491
492 scratch.clear();
493 scratch.extend_from_slice(state);
494 let mut of2 = vec![0.0f32; vd];
495 gdn_step(&qkv2, &z2, &a2, &b2, w, cfg, scratch, &mut of2);
496
497 let mut out1 = vec![0.0f32; cfg.hidden_size];
498 let mut out2 = vec![0.0f32; cfg.hidden_size];
499 w.out_proj.matvec2(&of1, &of2, &mut out1, &mut out2, pool);
500 (out1, out2)
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506
507 fn tiny() -> (VmfPhaseWeights, VmfPhaseCfg) {
508 let cfg = VmfPhaseCfg {
509 num_heads: 2,
510 nphase: 3,
511 value_head_dim: 4,
512 hidden_size: 8,
513 phase_mass: 0.0,
514 };
515 let synth = |rows: usize, cols: usize, salt: usize| {
516 QTensor::from_f32(
517 (0..rows * cols)
518 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
519 .collect(),
520 rows,
521 cols,
522 )
523 };
524 let w = VmfPhaseWeights {
525 thq: synth(cfg.num_heads * cfg.nphase, cfg.hidden_size, 1),
526 thk: synth(cfg.num_heads * cfg.nphase, cfg.hidden_size, 2),
527 v_proj: synth(cfg.num_heads * cfg.value_head_dim, cfg.hidden_size, 3),
528 out_proj: synth(cfg.hidden_size, cfg.num_heads * cfg.value_head_dim, 4),
529 decay: (0..cfg.num_heads * 2 * cfg.nphase)
530 .map(|i| 0.9 + 0.005 * (i % 10) as f64)
531 .collect(),
532 };
533 (w, cfg)
534 }
535
536 #[test]
537 fn state_persists_and_changes_output() {
538 let (w, cfg) = tiny();
539 let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
540 let mut state = Vec::new();
541 let o1 = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
542 let o2 = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
543 assert!(o1.iter().zip(&o2).any(|(a, b)| (a - b).abs() > 1e-6));
545 assert_eq!(state.len(), cfg.state_len());
546 }
547
548 #[test]
552 fn phase_mass_zero_is_noop_and_positive_shifts() {
553 let (w, cfg0) = tiny();
554 let mut cfg_m = cfg0.clone();
555 cfg_m.phase_mass = 1.0;
556 let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.4).sin()).collect();
557
558 let mut s0 = Vec::new();
559 let base = vmf_phase_forward(&x, &w, &cfg0, &mut s0, None);
560 let mut s0b = Vec::new();
562 let base2 = vmf_phase_forward(&x, &w, &cfg0, &mut s0b, None);
563 assert_eq!(base, base2, "mass=0 must be deterministic/no-op");
564 let mut sm = Vec::new();
566 let massed = vmf_phase_forward(&x, &w, &cfg_m, &mut sm, None);
567 assert!(
568 base.iter().zip(&massed).any(|(a, b)| (a - b).abs() > 1e-5),
569 "mass>0 must change the output"
570 );
571 assert!(massed.iter().all(|v| v.is_finite()));
572 }
573
574 #[test]
575 fn pair_matches_two_singles_bitexact() {
576 let (w, cfg) = tiny();
577 let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
578 let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
579
580 let mut s_ref = Vec::new();
582 let r1 = vmf_phase_forward(&x1, &w, &cfg, &mut s_ref, None);
583 let r2 = vmf_phase_forward(&x2, &w, &cfg, &mut s_ref, None);
584
585 let mut s = Vec::new();
587 let mut scratch = Vec::new();
588 let (p1, p2) = vmf_phase_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
589 assert_eq!(r1, p1, "lane 1 must be bit-identical");
590 assert_eq!(r2, p2, "lane 2 must be bit-identical");
591 std::mem::swap(&mut s, &mut scratch);
593 assert_eq!(s, s_ref, "accepted state must equal sequential state");
594 }
595
596 #[test]
597 fn rejected_draft_leaves_state_at_lane1() {
598 let (w, cfg) = tiny();
599 let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.7).sin()).collect();
600 let x2 = vec![0.5f32; 8];
601
602 let mut s_ref = Vec::new();
603 let _ = vmf_phase_forward(&x1, &w, &cfg, &mut s_ref, None);
604
605 let mut s = Vec::new();
606 let mut scratch = Vec::new();
607 let _ = vmf_phase_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
608 assert_eq!(s, s_ref);
610 }
611
612 fn tiny_gdn() -> (GdnWeights, GdnCfg) {
615 let cfg = GdnCfg {
616 num_v_heads: 4,
617 num_k_heads: 2,
618 key_head_dim: 3,
619 value_head_dim: 5,
620 conv_kernel: 4,
621 hidden_size: 8,
622 rms_eps: 1e-6,
623 };
624 let c_dim = cfg.conv_dim();
625 let vd = cfg.num_v_heads * cfg.value_head_dim;
626 let synth = |rows: usize, cols: usize, salt: usize| {
627 QTensor::from_f32(
628 (0..rows * cols)
629 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
630 .collect(),
631 rows,
632 cols,
633 )
634 };
635 let vecf = |n: usize, salt: usize| -> Vec<f32> {
636 (0..n)
637 .map(|i| (((i * 11 + salt * 5) % 89) as f32 / 89.0 - 0.5) * 0.6)
638 .collect()
639 };
640 let w = GdnWeights {
641 in_proj_qkv: synth(c_dim, cfg.hidden_size, 1),
642 in_proj_z: synth(vd, cfg.hidden_size, 2),
643 in_proj_a: synth(cfg.num_v_heads, cfg.hidden_size, 3),
644 in_proj_b: synth(cfg.num_v_heads, cfg.hidden_size, 4),
645 conv1d: vecf(c_dim * cfg.conv_kernel, 5),
646 a_log: (0..cfg.num_v_heads).map(|i| 0.2 + 0.3 * i as f32).collect(),
647 dt_bias: vecf(cfg.num_v_heads, 6),
648 norm: vec![1.0; cfg.value_head_dim],
649 out_proj: synth(cfg.hidden_size, vd, 7),
650 };
651 (w, cfg)
652 }
653
654 #[test]
655 fn gdn_state_persists_and_changes_output() {
656 let (w, cfg) = tiny_gdn();
657 let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
658 let mut state = Vec::new();
659 let o1 = gdn_forward(&x, &w, &cfg, &mut state, None);
660 let o2 = gdn_forward(&x, &w, &cfg, &mut state, None);
661 assert!(o1.iter().zip(&o2).any(|(a, b)| (a - b).abs() > 1e-6));
662 assert_eq!(state.len(), cfg.state_len());
663 }
664
665 #[test]
666 fn gdn_pair_matches_two_singles_bitexact() {
667 let (w, cfg) = tiny_gdn();
668 let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
669 let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
670
671 let mut s_ref = Vec::new();
672 let r1 = gdn_forward(&x1, &w, &cfg, &mut s_ref, None);
673 let r2 = gdn_forward(&x2, &w, &cfg, &mut s_ref, None);
674
675 let mut s = Vec::new();
676 let mut scratch = Vec::new();
677 let (p1, p2) = gdn_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
678 assert_eq!(r1, p1, "lane 1 must be bit-identical");
679 assert_eq!(r2, p2, "lane 2 must be bit-identical");
680 std::mem::swap(&mut s, &mut scratch);
681 assert_eq!(s, s_ref, "accepted state must equal sequential state");
682 }
683
684 #[test]
685 fn gdn_rejected_draft_leaves_state_at_lane1() {
686 let (w, cfg) = tiny_gdn();
687 let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.7).sin()).collect();
688 let x2 = vec![0.5f32; 8];
689
690 let mut s_ref = Vec::new();
691 let _ = gdn_forward(&x1, &w, &cfg, &mut s_ref, None);
692
693 let mut s = Vec::new();
694 let mut scratch = Vec::new();
695 let _ = gdn_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
696 assert_eq!(s, s_ref);
697 }
698
699 #[test]
703 fn gdn_conv_ring_matches_explicit_causal_conv() {
704 let (w, cfg) = tiny_gdn();
705 let seq: Vec<Vec<f32>> = (0..6)
706 .map(|t| (0..8).map(|i| ((t * 8 + i) as f32 * 0.17).sin()).collect())
707 .collect();
708
709 let mut s_inc = Vec::new();
712 for (t, x) in seq.iter().enumerate() {
713 let inc = gdn_forward(x, &w, &cfg, &mut s_inc, None);
714 let mut s_replay = Vec::new();
715 let mut replay = Vec::new();
716 for xr in &seq[..=t] {
717 replay = gdn_forward(xr, &w, &cfg, &mut s_replay, None);
718 }
719 assert_eq!(inc, replay, "position {t}: ring must equal replay");
720 }
721 }
722}