1use frink_core::gdn::{delta_step, l2_normalize, DeltaDims, HeadMap};
51use frink_core::mamba2::{conv_step, softplus};
52use frink_core::matmul::rms_norm;
53use frink_core::recurrent_state::RecurrentState;
54use frink_core::weight_matrix::WeightMatrix;
55use frink_gguf::TensorSource;
56
57use crate::layer_shapes::AttnShape;
58use crate::loader::{load_f32_vec, load_weight_matrix, LoadError};
59
60pub const GROUPED_HEAD_ARCHITECTURES: &[&str] = &["qwen3next"];
65
66pub fn head_map(arch: &str) -> HeadMap {
68 if GROUPED_HEAD_ARCHITECTURES.contains(&arch) {
69 HeadMap::Grouped
70 } else {
71 HeadMap::Tiled
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct GdnHparams {
79 pub d_conv: usize,
80 pub head_dim: usize,
82 pub n_k_heads: usize,
84 pub n_v_heads: usize,
86 pub map: HeadMap,
87}
88
89impl GdnHparams {
90 pub fn read(file: &impl TensorSource, arch: &str) -> Result<Self, LoadError> {
91 let key = |k: &str| format!("{arch}.ssm.{k}");
92 let read = |k: &str| {
93 file.metadata_u64(&key(k))
94 .map(|v| v as usize)
95 .ok_or_else(|| LoadError::MissingHparam(key(k)))
96 };
97 let h = Self {
98 d_conv: read("conv_kernel")?,
99 head_dim: read("state_size")?,
100 n_k_heads: read("group_count")?,
101 n_v_heads: read("time_step_rank")?,
102 map: head_map(arch),
103 };
104 let d_inner = read("inner_size")?;
105 if h.d_conv < 2
106 || h.head_dim == 0
107 || h.n_k_heads == 0
108 || !h.n_v_heads.is_multiple_of(h.n_k_heads)
109 || d_inner != h.n_v_heads * h.head_dim
110 {
111 return Err(LoadError::UnsupportedFeature(
112 arch.to_string(),
113 format!(
114 "ssm.* hparams {h:?} with inner_size {d_inner}: qwen35.cpp:52-58 sizes the \
115 block as inner_size = time_step_rank * state_size with time_step_rank a \
116 multiple of group_count (delta-net-base.cpp:308)"
117 ),
118 ));
119 }
120 Ok(h)
121 }
122
123 pub fn key_dim(self) -> usize {
124 self.n_k_heads * self.head_dim
125 }
126
127 pub fn value_dim(self) -> usize {
128 self.n_v_heads * self.head_dim
129 }
130
131 pub fn conv_dim(self) -> usize {
133 2 * self.key_dim() + self.value_dim()
134 }
135
136 pub fn delta_dims(self) -> DeltaDims {
137 DeltaDims {
138 n_k_heads: self.n_k_heads,
139 n_v_heads: self.n_v_heads,
140 head_dim: self.head_dim,
141 map: self.map,
142 }
143 }
144
145 pub fn state_floats(self) -> (usize, usize) {
147 (
148 (self.d_conv - 1) * self.conv_dim(),
149 self.delta_dims().state_len(),
150 )
151 }
152}
153
154pub enum BetaAlpha {
156 Split {
159 beta: WeightMatrix,
160 alpha: WeightMatrix,
161 },
162 Fused { ba: WeightMatrix },
166}
167
168pub struct Gdn {
170 pub h: GdnHparams,
171 pub qkv: WeightMatrix,
173 pub z_proj: WeightMatrix,
175 pub conv1d: Vec<f32>,
177 pub dt_bias: Vec<f32>,
179 pub a: Vec<f32>,
181 pub beta_alpha: BetaAlpha,
182 pub norm: Vec<f32>,
184 pub out_proj: WeightMatrix,
186}
187
188impl Gdn {
189 pub fn load(
192 file: &impl TensorSource,
193 arch: &str,
194 layer: usize,
195 hidden_dim: usize,
196 ) -> Result<Self, LoadError> {
197 let h = GdnHparams::read(file, arch)?;
198 let name = |t: &str| format!("blk.{layer}.{t}");
199 let matrix = |t: &str, rows: usize, cols: usize| -> Result<WeightMatrix, LoadError> {
200 let m = load_weight_matrix(file, &name(t))?;
201 if m.rows() != rows || m.cols() != cols {
202 return Err(LoadError::UnsupportedFeature(
203 name(t),
204 format!(
205 "{}x{}; qwen35.cpp:66-74 sizes it {rows}x{cols}",
206 m.rows(),
207 m.cols()
208 ),
209 ));
210 }
211 Ok(m)
212 };
213 let vector = |t: &str, len: usize| -> Result<Vec<f32>, LoadError> {
214 let v = load_f32_vec(file, &name(t))?;
215 if v.len() != len {
216 return Err(LoadError::UnsupportedFeature(
217 name(t),
218 format!("{} elements; qwen35.cpp:66-74 sizes it {len}", v.len()),
219 ));
220 }
221 Ok(v)
222 };
223 if file.find_tensor(&name("attn_qkv.weight")).is_none() {
226 return Err(LoadError::UnsupportedFeature(
227 name("attn_qkv.weight"),
228 "absent: the gated delta net's fused q/k/v projection (qwen35.cpp:66); the \
229 legacy `ssm_in` / `ssm_ba` spelling (qwen3next.cpp:88-95) is not served"
230 .to_string(),
231 ));
232 }
233 let beta_alpha = if file.find_tensor(&name("ssm_ba.weight")).is_some() {
234 BetaAlpha::Fused {
235 ba: matrix("ssm_ba.weight", 2 * h.n_v_heads, hidden_dim)?,
236 }
237 } else {
238 BetaAlpha::Split {
239 beta: matrix("ssm_beta.weight", h.n_v_heads, hidden_dim)?,
240 alpha: matrix("ssm_alpha.weight", h.n_v_heads, hidden_dim)?,
241 }
242 };
243 Ok(Self {
244 h,
245 qkv: matrix("attn_qkv.weight", h.conv_dim(), hidden_dim)?,
246 z_proj: matrix("attn_gate.weight", h.value_dim(), hidden_dim)?,
247 conv1d: vector("ssm_conv1d.weight", h.d_conv * h.conv_dim())?,
248 dt_bias: vector("ssm_dt.bias", h.n_v_heads)?,
249 a: vector("ssm_a", h.n_v_heads)?,
250 beta_alpha,
251 norm: vector("ssm_norm.weight", h.head_dim)?,
252 out_proj: matrix("ssm_out.weight", hidden_dim, h.value_dim())?,
253 })
254 }
255
256 pub fn zero_state(&self) -> RecurrentState {
258 let (conv, ssm) = self.h.state_floats();
259 RecurrentState::zeros(conv, ssm)
260 }
261
262 pub fn forward_rows(
265 &self,
266 normed: &[f32],
267 rows: usize,
268 state: &mut RecurrentState,
269 rms_eps: f32,
270 ) -> Vec<f32> {
271 let h = self.h;
272 let n_embd = self.out_proj.rows();
273 assert_eq!(normed.len(), rows * n_embd);
274 let (conv_len, ssm_len) = h.state_floats();
275 assert_eq!(
276 state.conv.len(),
277 conv_len,
278 "conv state sized by these weights"
279 );
280 assert_eq!(
281 state.ssm.len(),
282 ssm_len,
283 "delta state sized by these weights"
284 );
285 let (s, n_k, n_v) = (h.head_dim, h.n_k_heads, h.n_v_heads);
286 let (key_dim, value_dim, conv_dim) = (h.key_dim(), h.value_dim(), h.conv_dim());
287 let dims = h.delta_dims();
288 let project = |m: &WeightMatrix| {
289 if rows == 1 {
290 m.apply(normed)
291 } else {
292 m.apply_batch(normed, rows)
293 }
294 };
295 let (qkv_all, z_all) = if rows == 1 {
304 WeightMatrix::apply_pair(&self.qkv, &self.z_proj, normed)
305 } else {
306 WeightMatrix::apply_batch_pair_with_acts(&self.qkv, &self.z_proj, normed, rows, None)
308 };
309 let (beta_all, alpha_all) = match &self.beta_alpha {
312 BetaAlpha::Split { beta, alpha } => (project(beta), project(alpha)),
313 BetaAlpha::Fused { ba } => {
314 let mixed = project(ba);
315 let ratio = n_v / n_k;
316 let mut beta_all = vec![0.0f32; rows * n_v];
317 let mut alpha_all = vec![0.0f32; rows * n_v];
318 for r in 0..rows {
319 let row = &mixed[r * 2 * n_v..(r + 1) * 2 * n_v];
320 for hd in 0..n_v {
321 let base = (hd / ratio) * 2 * ratio + hd % ratio;
324 beta_all[r * n_v + hd] = row[base];
325 alpha_all[r * n_v + hd] = row[base + ratio];
326 }
327 }
328 (beta_all, alpha_all)
329 }
330 };
331 let mut ys = vec![0.0f32; rows * value_dim];
332 let mut conv_out = vec![0.0f32; conv_dim];
333 let mut o = vec![0.0f32; value_dim];
334 let mut g = vec![0.0f32; n_v];
335 let mut beta = vec![0.0f32; n_v];
336 #[cfg(feature = "metal")]
361 if rows == 1 {
362 if let Some(out) = self.device_branch_full(
363 state,
364 rms_eps,
365 None,
366 None,
367 Some((&qkv_all, &z_all, &beta_all, &alpha_all)),
368 ) {
369 return out;
370 }
371 }
372 if rows > 1 {
373 let (q_all, k_all, v_all, g_all, beta_gate) =
374 self.conv_and_gates_for_rows(rows, &qkv_all, &beta_all, &alpha_all, state, rms_eps);
375 let mut o_all = vec![0.0f32; rows * value_dim];
376 frink_core::gdn_chunk::delta_chunk_rows(
377 dims,
378 rows,
379 &mut state.ssm,
380 &q_all,
381 &k_all,
382 &v_all,
383 &g_all,
384 &beta_gate,
385 &mut o_all,
386 );
387 for r in 0..rows {
388 let o = &o_all[r * value_dim..(r + 1) * value_dim];
389 let y = &mut ys[r * value_dim..(r + 1) * value_dim];
390 let z = &z_all[r * value_dim..(r + 1) * value_dim];
391 for hd in 0..n_v {
392 let normed_head = rms_norm(&o[hd * s..(hd + 1) * s], &self.norm, rms_eps);
393 for i in 0..s {
394 y[hd * s + i] = normed_head[i] * silu(z[hd * s + i]);
395 }
396 }
397 }
398 return self.out_proj.apply_batch(&ys, rows);
399 }
400 for r in 0..rows {
401 for hd in 0..n_v {
403 beta[hd] = sigmoid(beta_all[r * n_v + hd]);
404 g[hd] = softplus(alpha_all[r * n_v + hd] + self.dt_bias[hd]) * self.a[hd];
405 }
406 conv_step(
408 &mut state.conv,
409 &self.conv1d,
410 h.d_conv,
411 &qkv_all[r * conv_dim..(r + 1) * conv_dim],
412 &mut conv_out,
413 );
414 for x in conv_out.iter_mut() {
415 *x = silu(*x);
416 }
417 let (q, rest) = conv_out.split_at_mut(key_dim);
418 let (k, v) = rest.split_at_mut(key_dim);
419 for hd in 0..n_k {
421 l2_normalize(&mut q[hd * s..(hd + 1) * s], rms_eps);
422 l2_normalize(&mut k[hd * s..(hd + 1) * s], rms_eps);
423 }
424 delta_step(dims, &mut state.ssm, q, k, v, &g, &beta, &mut o);
437 let y = &mut ys[r * value_dim..(r + 1) * value_dim];
440 let z = &z_all[r * value_dim..(r + 1) * value_dim];
441 for hd in 0..n_v {
442 let normed_head = rms_norm(&o[hd * s..(hd + 1) * s], &self.norm, rms_eps);
443 for i in 0..s {
444 y[hd * s + i] = normed_head[i] * silu(z[hd * s + i]);
445 }
446 }
447 }
448 if rows == 1 {
449 self.out_proj.apply(&ys)
450 } else {
451 self.out_proj.apply_batch(&ys, rows)
452 }
453 }
454}
455
456impl Gdn {
457 #[cfg(feature = "metal")]
466 pub fn fused_layer(
467 &self,
468 attn_norm: &[f32],
469 normed: &[f32],
470 state: &mut RecurrentState,
471 rms_eps: f32,
472 ffn: &crate::fused_layer::LayerFfnParts<'_>,
473 residual: &[f32],
474 ) -> Option<Vec<f32>> {
475 let BetaAlpha::Split { beta, alpha } = &self.beta_alpha else {
476 return None;
479 };
480 if let Some(head) = crate::fused_layer::LayerHeadParts::for_block(
484 attn_norm,
485 rms_eps,
486 &self.qkv,
487 &self.z_proj,
488 beta,
489 alpha,
490 ) {
491 if let Some(out) =
492 self.device_branch_full(state, rms_eps, Some(&head), Some((ffn, residual)), None)
493 {
494 return Some(out);
495 }
496 }
497 let (qkv_all, z_all) = WeightMatrix::apply_pair(&self.qkv, &self.z_proj, normed);
499 let (beta_all, alpha_all) = (beta.apply(normed), alpha.apply(normed));
500 self.device_branch_full(
501 state,
502 rms_eps,
503 None,
504 Some((ffn, residual)),
505 Some((&qkv_all, &z_all, &beta_all, &alpha_all)),
506 )
507 }
508
509 #[cfg(feature = "metal")]
521 #[cfg(feature = "metal")]
530 fn with_branch_weights<R>(
531 &self,
532 rms_eps: f32,
533 head: Option<&crate::fused_layer::LayerHeadParts<'_>>,
534 ffn: Option<&crate::fused_layer::LayerFfnParts<'_>>,
535 f: impl FnOnce(&frink_metal::gdn_branch::BranchWeights<'_>) -> R,
536 ) -> Option<R> {
537 if !frink_core::weight_matrix::metal_dense_enabled() {
538 return None;
539 }
540 if matches!(self.beta_alpha, BetaAlpha::Fused { .. }) {
541 return None;
542 }
543 let h = self.h;
544 let (base, fold) = self.out_proj.launch_parts();
545 let out_proj = crate::metal_launch::matvec(base)?;
546 let fold_y = match fold {
547 None => None,
548 Some(fd) => Some(fd.metal_plan(h.value_dim())?),
549 };
550 let ffn_launches = match ffn {
551 None => None,
552 Some(parts) => Some(parts.launches()?),
553 };
554 let head_launches = match head {
555 None => None,
556 Some(parts) => Some(parts.launches()?),
557 };
558 let w = frink_metal::gdn_branch::BranchWeights {
559 shape: frink_metal::gdn::DeltaShape {
560 n_k_heads: h.n_k_heads,
561 n_v_heads: h.n_v_heads,
562 head_dim: h.head_dim,
563 map: match h.map {
564 HeadMap::Tiled => frink_metal::gdn::HeadMapKind::Tiled,
565 HeadMap::Grouped => frink_metal::gdn::HeadMapKind::Grouped,
566 },
567 },
568 head: frink_metal::gdn_head::HeadShape {
569 n_k_heads: h.n_k_heads,
570 n_v_heads: h.n_v_heads,
571 head_dim: h.head_dim,
572 d_conv: h.d_conv,
573 },
574 conv1d: &self.conv1d,
575 dt_bias: &self.dt_bias,
576 a: &self.a,
577 ssm_norm: &self.norm,
578 eps: rms_eps,
579 out_proj: &out_proj,
580 fold_y: fold_y.as_ref(),
581 ffn: ffn_launches.as_ref().map(|l| l.as_metal()),
582 head_in: head_launches.as_ref().map(|l| l.as_metal()),
583 };
584 if !w.is_supported() {
585 return None;
586 }
587 Some(f(&w))
588 }
589
590 #[cfg(feature = "metal")]
599 pub unsafe fn run_layer(
600 &self,
601 run: &mut frink_metal::gdn_branch::GdnRun,
602 attn_norm: &[f32],
603 rms_eps: f32,
604 ffn: &crate::fused_layer::LayerFfnParts<'_>,
605 state: &mut RecurrentState,
606 ) -> Option<()> {
607 let BetaAlpha::Split { beta, alpha } = &self.beta_alpha else {
608 return None;
609 };
610 let head = crate::fused_layer::LayerHeadParts::for_block(
611 attn_norm,
612 rms_eps,
613 &self.qkv,
614 &self.z_proj,
615 beta,
616 alpha,
617 )?;
618 let conv_len = state.conv.len();
619 let (ssm_bytes, ssm_ptr) = (state.ssm.alloc_bytes(), state.ssm.as_ptr());
620 let (conv_bytes, conv_ptr) = (state.conv.alloc_bytes(), state.conv.as_ptr());
621 self.with_branch_weights(rms_eps, Some(&head), Some(ffn), |w| {
622 unsafe { run.layer(w, ssm_ptr, ssm_bytes, conv_ptr, conv_bytes, conv_len) }
624 })?
625 .ok()
626 }
627
628 #[cfg(feature = "metal")]
629 #[allow(clippy::type_complexity)]
630 fn device_branch_full(
631 &self,
632 state: &mut RecurrentState,
633 rms_eps: f32,
634 head: Option<&crate::fused_layer::LayerHeadParts<'_>>,
635 rest: Option<(&crate::fused_layer::LayerFfnParts<'_>, &[f32])>,
636 host_proj: Option<(&[f32], &[f32], &[f32], &[f32])>,
637 ) -> Option<Vec<f32>> {
638 if !frink_core::weight_matrix::metal_dense_enabled() {
639 return None;
640 }
641 if matches!(self.beta_alpha, BetaAlpha::Fused { .. }) {
646 return None;
647 }
648 let h = self.h;
649 let (base, fold) = self.out_proj.launch_parts();
650 let out_proj = crate::metal_launch::matvec(base)?;
651 let fold_y = match fold {
652 None => None,
653 Some(f) => Some(f.metal_plan(h.value_dim())?),
654 };
655 let w = frink_metal::gdn_branch::BranchWeights {
656 shape: frink_metal::gdn::DeltaShape {
657 n_k_heads: h.n_k_heads,
658 n_v_heads: h.n_v_heads,
659 head_dim: h.head_dim,
660 map: match h.map {
661 HeadMap::Tiled => frink_metal::gdn::HeadMapKind::Tiled,
662 HeadMap::Grouped => frink_metal::gdn::HeadMapKind::Grouped,
663 },
664 },
665 head: frink_metal::gdn_head::HeadShape {
666 n_k_heads: h.n_k_heads,
667 n_v_heads: h.n_v_heads,
668 head_dim: h.head_dim,
669 d_conv: h.d_conv,
670 },
671 conv1d: &self.conv1d,
672 dt_bias: &self.dt_bias,
673 a: &self.a,
674 ssm_norm: &self.norm,
675 eps: rms_eps,
676 out_proj: &out_proj,
677 fold_y: fold_y.as_ref(),
678 ffn: None,
679 head_in: None,
680 };
681 let ffn_launches = match rest {
684 None => None,
685 Some((parts, _)) => Some(parts.launches()?),
686 };
687 let head_launches = match head {
688 None => None,
689 Some(parts) => Some(parts.launches()?),
690 };
691 let w = frink_metal::gdn_branch::BranchWeights {
692 ffn: ffn_launches.as_ref().map(|l| l.as_metal()),
693 head_in: head_launches.as_ref().map(|l| l.as_metal()),
694 ..w
695 };
696 if !w.is_supported() {
697 return None;
698 }
699 let conv_len = state.conv.len();
700 let (ssm_bytes, ssm_ptr) = (state.ssm.alloc_bytes(), state.ssm.as_ptr());
704 let (conv_bytes, conv_ptr) = (state.conv.alloc_bytes(), state.conv.as_ptr());
705 unsafe {
709 frink_metal::gdn_branch::launch_gdn_branch(
710 &w,
711 ssm_ptr,
712 ssm_bytes,
713 conv_ptr,
714 conv_bytes,
715 conv_len,
716 host_proj.map(|(q, _, _, _)| q),
717 host_proj.map(|(_, z, _, _)| z),
718 host_proj.map(|(_, _, b, _)| b),
719 host_proj.map(|(_, _, _, a)| a),
720 rest.map(|(_, residual)| residual),
723 None,
727 )
728 }
729 .ok()
730 }
731
732 #[allow(clippy::type_complexity)]
739 fn conv_and_gates_for_rows(
740 &self,
741 rows: usize,
742 qkv_all: &[f32],
743 beta_all: &[f32],
744 alpha_all: &[f32],
745 state: &mut RecurrentState,
746 rms_eps: f32,
747 ) -> (Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>) {
748 let h = self.h;
749 let (s, n_k, n_v) = (h.head_dim, h.n_k_heads, h.n_v_heads);
750 let (key_dim, value_dim, conv_dim) = (h.key_dim(), h.value_dim(), h.conv_dim());
751 let mut q_all = vec![0.0f32; rows * key_dim];
752 let mut k_all = vec![0.0f32; rows * key_dim];
753 let mut v_all = vec![0.0f32; rows * value_dim];
754 let mut g_all = vec![0.0f32; rows * n_v];
755 let mut beta_gate = vec![0.0f32; rows * n_v];
756 let mut conv_out = vec![0.0f32; conv_dim];
757 for r in 0..rows {
758 for hd in 0..n_v {
759 beta_gate[r * n_v + hd] = sigmoid(beta_all[r * n_v + hd]);
760 g_all[r * n_v + hd] =
761 softplus(alpha_all[r * n_v + hd] + self.dt_bias[hd]) * self.a[hd];
762 }
763 conv_step(
764 &mut state.conv,
765 &self.conv1d,
766 h.d_conv,
767 &qkv_all[r * conv_dim..(r + 1) * conv_dim],
768 &mut conv_out,
769 );
770 for x in conv_out.iter_mut() {
771 *x = silu(*x);
772 }
773 let (q, rest) = conv_out.split_at_mut(key_dim);
774 let (k, v) = rest.split_at_mut(key_dim);
775 for hd in 0..n_k {
776 l2_normalize(&mut q[hd * s..(hd + 1) * s], rms_eps);
777 l2_normalize(&mut k[hd * s..(hd + 1) * s], rms_eps);
778 }
779 q_all[r * key_dim..(r + 1) * key_dim].copy_from_slice(q);
780 k_all[r * key_dim..(r + 1) * key_dim].copy_from_slice(k);
781 v_all[r * value_dim..(r + 1) * value_dim].copy_from_slice(v);
782 }
783 (q_all, k_all, v_all, g_all, beta_gate)
784 }
785}
786
787#[inline]
788fn silu(x: f32) -> f32 {
789 x / (1.0 + (-x).exp())
790}
791
792#[inline]
793fn sigmoid(x: f32) -> f32 {
794 1.0 / (1.0 + (-x).exp())
795}
796
797pub const INTERVAL_RECURRENT_ARCHITECTURES: &[(&str, usize, AttnShape)] = &[
808 ("qwen35", 4, AttnShape::Gdn),
809 ("qwen35moe", 4, AttnShape::Gdn),
810 ("qwen3next", 4, AttnShape::Gdn),
811 ("minimax-01", 8, AttnShape::Lightning),
816];
817
818#[derive(Debug, Clone, PartialEq, Eq)]
825pub struct RecurrentMask {
826 pub layers: Vec<bool>,
828 pub block: AttnShape,
829}
830
831pub fn recurrent_layers(
841 file: &impl TensorSource,
842 arch: &str,
843 block_count: usize,
844 n_layers: usize,
845) -> Result<Option<RecurrentMask>, LoadError> {
846 let Some((_, default_interval, block)) = INTERVAL_RECURRENT_ARCHITECTURES
847 .iter()
848 .find(|(a, _, _)| *a == arch)
849 else {
850 return Ok(None);
851 };
852 let key = format!("{arch}.attention.recurrent_layers");
853 if let Some(frink_gguf::GgufValue::Array(items)) = file.metadata(&key) {
854 if items.len() != block_count {
855 return Err(LoadError::UnsupportedFeature(
856 key,
857 format!(
858 "{} entries for block_count {block_count}; llama.cpp reads it at n_layer_all \
859 length (qwen35.cpp:17)",
860 items.len()
861 ),
862 ));
863 }
864 let mut out = Vec::with_capacity(n_layers);
865 for (il, item) in items.iter().enumerate().take(n_layers) {
866 out.push(item.as_bool().ok_or_else(|| {
867 LoadError::UnsupportedFeature(key.clone(), format!("entry {il} is not a bool"))
868 })?);
869 }
870 return Ok(Some(RecurrentMask {
871 layers: out,
872 block: *block,
873 }));
874 }
875 let interval = file
876 .metadata_u64(&format!("{arch}.full_attention_interval"))
877 .unwrap_or(*default_interval as u64) as usize;
878 if interval == 0 {
879 return Err(LoadError::UnsupportedFeature(
880 format!("{arch}.full_attention_interval"),
881 "0: qwen35.cpp:22 takes `(i + 1) % interval`".to_string(),
882 ));
883 }
884 Ok(Some(RecurrentMask {
885 layers: (0..n_layers)
886 .map(|i| !(i + 1).is_multiple_of(interval))
887 .collect(),
888 block: *block,
889 }))
890}
891
892#[cfg(test)]
893mod tests {
894 use super::*;
895 use frink_core::Tensor;
896
897 fn hp() -> GdnHparams {
898 GdnHparams {
899 d_conv: 3,
900 head_dim: 2,
901 n_k_heads: 1,
902 n_v_heads: 2,
903 map: HeadMap::Tiled,
904 }
905 }
906
907 #[test]
908 fn the_widths_are_the_graph_s() {
909 let h = hp();
910 assert_eq!((h.key_dim(), h.value_dim(), h.conv_dim()), (2, 4, 8));
911 assert_eq!(h.state_floats(), (2 * 8, 2 * 2 * 2));
912 }
913
914 #[cfg(feature = "metal")]
924 #[test]
925 #[ignore = "needs a real Metal-capable GPU; run manually with --ignored on Apple Silicon"]
926 fn the_device_head_matches_this_one() {
927 use frink_metal::gdn_head::{launch_gdn_head, HeadShape};
928
929 for h in [
930 GdnHparams {
931 d_conv: 4,
932 head_dim: 128,
933 n_k_heads: 4,
934 n_v_heads: 48,
935 map: HeadMap::Tiled,
936 },
937 GdnHparams {
938 d_conv: 2,
939 head_dim: 4,
940 n_k_heads: 1,
941 n_v_heads: 2,
942 map: HeadMap::Grouped,
943 },
944 ] {
945 let n_embd = 8;
946 let mut seed = 424_242u32;
947 let mut rnd = |n: usize, scale: f32| -> Vec<f32> {
948 (0..n)
949 .map(|_| {
950 seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
951 (((seed >> 9) as f32 / (1u32 << 23) as f32) - 0.5) * scale
952 })
953 .collect()
954 };
955 let mat = |rows: usize, cols: usize, v: Vec<f32>| {
956 WeightMatrix::F32(Tensor::new(v, vec![rows, cols]))
957 };
958 let conv_dim = h.conv_dim();
959 let m = Gdn {
960 h,
961 qkv: mat(conv_dim, n_embd, rnd(conv_dim * n_embd, 1.0)),
962 z_proj: mat(h.value_dim(), n_embd, rnd(h.value_dim() * n_embd, 1.0)),
963 conv1d: rnd(h.d_conv * conv_dim, 1.0),
964 dt_bias: rnd(h.n_v_heads, 1.0),
965 a: rnd(h.n_v_heads, 1.0),
966 beta_alpha: BetaAlpha::Split {
967 beta: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd, 1.0)),
968 alpha: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd, 1.0)),
969 },
970 norm: rnd(h.head_dim, 1.0),
971 out_proj: mat(n_embd, h.value_dim(), rnd(n_embd * h.value_dim(), 1.0)),
972 };
973 let qkv = rnd(conv_dim, 1.0);
974 let beta_in = rnd(h.n_v_heads, 4.0);
975 let alpha_in = rnd(h.n_v_heads, 400.0);
982 let conv0 = rnd(h.state_floats().0, 1.0);
983 let eps = 1e-6f32;
984
985 let aligned = |v: &[f32]| {
986 let mut a = frink_core::recurrent_state::AlignedF32::zeros(v.len());
987 a.copy_from_slice(v);
988 a
989 };
990 let mut host_state = RecurrentState {
991 conv: aligned(&conv0),
992 ssm: frink_core::recurrent_state::AlignedF32::zeros(h.state_floats().1),
993 };
994 let (hq, hk, hv, hg, hbeta) =
995 m.conv_and_gates_for_rows(1, &qkv, &beta_in, &alpha_in, &mut host_state, eps);
996
997 let mut device_conv = conv0;
998 let (dq, dk, dv, dg, dbeta) = launch_gdn_head(
999 HeadShape {
1000 n_k_heads: h.n_k_heads,
1001 n_v_heads: h.n_v_heads,
1002 head_dim: h.head_dim,
1003 d_conv: h.d_conv,
1004 },
1005 &mut device_conv,
1006 &m.conv1d,
1007 &qkv,
1008 &beta_in,
1009 &alpha_in,
1010 &m.dt_bias,
1011 &m.a,
1012 eps,
1013 )
1014 .expect("the kernels launch");
1015
1016 let tol = 2e-5;
1017 for (what, a, b) in [
1018 ("q", &dq, &hq),
1019 ("k", &dk, &hk),
1020 ("v", &dv, &hv),
1021 ("g", &dg, &hg),
1022 ("beta", &dbeta, &hbeta),
1023 ("conv state", &device_conv, &host_state.conv[..].to_vec()),
1024 ] {
1025 assert_eq!(a.len(), b.len(), "{what} length");
1026 for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
1027 assert!(
1028 (x - y).abs() <= tol * y.abs().max(1.0),
1029 "{}x{} {what}[{i}]: device={x} host={y}",
1030 h.n_v_heads,
1031 h.head_dim
1032 );
1033 }
1034 }
1035 }
1036 }
1037
1038 #[cfg(feature = "metal")]
1048 #[test]
1049 #[ignore = "needs a real Metal-capable GPU; run manually with --ignored on Apple Silicon"]
1050 fn the_device_branch_matches_the_host_branch() {
1051 use frink_core::gdn::delta_step;
1052
1053 for h in [
1054 GdnHparams {
1055 d_conv: 4,
1056 head_dim: 128,
1057 n_k_heads: 4,
1058 n_v_heads: 48,
1059 map: HeadMap::Tiled,
1060 },
1061 GdnHparams {
1062 d_conv: 2,
1063 head_dim: 8,
1064 n_k_heads: 2,
1065 n_v_heads: 4,
1066 map: HeadMap::Grouped,
1067 },
1068 ] {
1069 let n_embd = 16;
1070 let mut seed = 777_001u32;
1071 let mut rnd = |n: usize, scale: f32| -> Vec<f32> {
1072 (0..n)
1073 .map(|_| {
1074 seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
1075 (((seed >> 9) as f32 / (1u32 << 23) as f32) - 0.5) * scale
1076 })
1077 .collect()
1078 };
1079 let mat = |rows: usize, cols: usize, v: Vec<f32>| {
1080 WeightMatrix::F32(Tensor::new(v, vec![rows, cols]))
1081 };
1082 let (key_dim, value_dim, conv_dim) = (h.key_dim(), h.value_dim(), h.conv_dim());
1083 let m = Gdn {
1084 h,
1085 qkv: mat(conv_dim, n_embd, rnd(conv_dim * n_embd, 1.0)),
1086 z_proj: mat(value_dim, n_embd, rnd(value_dim * n_embd, 1.0)),
1087 conv1d: rnd(h.d_conv * conv_dim, 1.0),
1088 dt_bias: rnd(h.n_v_heads, 1.0),
1089 a: rnd(h.n_v_heads, 1.0),
1090 beta_alpha: BetaAlpha::Split {
1091 beta: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd, 1.0)),
1092 alpha: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd, 1.0)),
1093 },
1094 norm: rnd(h.head_dim, 1.0),
1095 out_proj: mat(n_embd, value_dim, rnd(n_embd * value_dim, 1.0)),
1096 };
1097 let (conv_len, ssm_len) = h.state_floats();
1098 let conv0 = rnd(conv_len, 1.0);
1099 let ssm0 = rnd(ssm_len, 0.5);
1100 let normed = rnd(n_embd, 1.0);
1101 let eps = 1e-6f32;
1102 let state0 = || RecurrentState {
1103 conv: {
1104 let mut a = frink_core::recurrent_state::AlignedF32::zeros(conv_len);
1105 a.copy_from_slice(&conv0);
1106 a
1107 },
1108 ssm: {
1109 let mut a = frink_core::recurrent_state::AlignedF32::zeros(ssm_len);
1110 a.copy_from_slice(&ssm0);
1111 a
1112 },
1113 };
1114
1115 let mut host_state = state0();
1118 let qkv_all = m.qkv.apply(&normed);
1119 let z_all = m.z_proj.apply(&normed);
1120 let (beta_all, alpha_all) = match &m.beta_alpha {
1121 BetaAlpha::Split { beta, alpha } => (beta.apply(&normed), alpha.apply(&normed)),
1122 BetaAlpha::Fused { .. } => unreachable!("split above"),
1123 };
1124 let (q, k, v, g, beta) =
1125 m.conv_and_gates_for_rows(1, &qkv_all, &beta_all, &alpha_all, &mut host_state, eps);
1126 let mut o = vec![0.0f32; value_dim];
1127 delta_step(
1128 h.delta_dims(),
1129 &mut host_state.ssm,
1130 &q,
1131 &k,
1132 &v,
1133 &g,
1134 &beta,
1135 &mut o,
1136 );
1137 let mut ys = vec![0.0f32; value_dim];
1138 for hd in 0..h.n_v_heads {
1139 let normed_head =
1140 rms_norm(&o[hd * h.head_dim..(hd + 1) * h.head_dim], &m.norm, eps);
1141 for i in 0..h.head_dim {
1142 ys[hd * h.head_dim + i] =
1143 normed_head[i] * super::silu(z_all[hd * h.head_dim + i]);
1144 }
1145 }
1146 let host_out = m.out_proj.apply(&ys);
1147
1148 let mut device_state = state0();
1150 let device_out = m
1151 .device_branch_full(
1152 &mut device_state,
1153 eps,
1154 None,
1155 None,
1156 Some((&qkv_all, &z_all, &beta_all, &alpha_all)),
1157 )
1158 .expect("this shape is one the kernels serve");
1159
1160 let tol = 2e-4;
1161 for (what, a, b) in [
1162 ("out", &device_out, &host_out),
1163 (
1164 "conv state",
1165 &device_state.conv[..].to_vec(),
1166 &host_state.conv[..].to_vec(),
1167 ),
1168 ] {
1169 assert_eq!(a.len(), b.len(), "{what} length");
1170 for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
1171 assert!(
1172 (x - y).abs() <= tol * y.abs().max(1.0),
1173 "{}x{} {what}[{i}]: device={x} host={y}",
1174 h.n_v_heads,
1175 h.head_dim
1176 );
1177 }
1178 }
1179 for (i, (x, y)) in device_state
1180 .ssm
1181 .iter()
1182 .zip(host_state.ssm.iter())
1183 .enumerate()
1184 {
1185 assert!(
1186 (x - y).abs() <= tol * y.abs().max(1.0),
1187 "{}x{} ssm state[{i}]: device={x} host={y}",
1188 h.n_v_heads,
1189 h.head_dim
1190 );
1191 }
1192 let _ = key_dim;
1193 }
1194 }
1195
1196 #[cfg(feature = "metal")]
1205 #[test]
1206 #[ignore = "needs a real Metal-capable GPU; run manually with --ignored on Apple Silicon"]
1207 fn the_fused_layer_matches_the_host_layer() {
1208 use crate::fused_layer::{LayerFfnParts, LayerHeadParts};
1209
1210 let h = GdnHparams {
1211 d_conv: 4,
1212 head_dim: 128,
1213 n_k_heads: 4,
1214 n_v_heads: 48,
1215 map: HeadMap::Tiled,
1216 };
1217 let (n_embd, ffn_dim) = (64usize, 96usize);
1218 let mut seed = 31_337u32;
1219 let mut rnd = |n: usize, scale: f32| -> Vec<f32> {
1220 (0..n)
1221 .map(|_| {
1222 seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
1223 (((seed >> 9) as f32 / (1u32 << 23) as f32) - 0.5) * scale
1224 })
1225 .collect()
1226 };
1227 let mat = |rows: usize, cols: usize, v: Vec<f32>| {
1228 WeightMatrix::F32(Tensor::new(v, vec![rows, cols]))
1229 };
1230 let (value_dim, conv_dim) = (h.value_dim(), h.conv_dim());
1231 let m = Gdn {
1232 h,
1233 qkv: mat(conv_dim, n_embd, rnd(conv_dim * n_embd, 1.0)),
1234 z_proj: mat(value_dim, n_embd, rnd(value_dim * n_embd, 1.0)),
1235 conv1d: rnd(h.d_conv * conv_dim, 1.0),
1236 dt_bias: rnd(h.n_v_heads, 1.0),
1237 a: rnd(h.n_v_heads, 1.0),
1238 beta_alpha: BetaAlpha::Split {
1239 beta: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd, 1.0)),
1240 alpha: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd, 1.0)),
1241 },
1242 norm: rnd(h.head_dim, 1.0),
1243 out_proj: mat(n_embd, value_dim, rnd(n_embd * value_dim, 1.0)),
1244 };
1245 let signs: std::sync::Arc<[f32]> = rnd(n_embd, 2.0)
1252 .iter()
1253 .map(|x| if *x < 0.0 { -1.0f32 } else { 1.0 })
1254 .collect::<Vec<f32>>()
1255 .into();
1256 let fold = std::sync::Arc::new(frink_core::weight_matrix::hadamard::HadamardFold {
1257 block: 16,
1258 signs: Some(signs),
1259 perm: None,
1260 site: frink_core::weight_matrix::hadamard::FoldSite::Input,
1261 });
1262 let mut m = m;
1263 m.qkv.fold_hadamard(fold.clone());
1264 m.z_proj.fold_hadamard(fold.clone());
1265 let m = m;
1266 let attn_norm = rnd(n_embd, 0.5).iter().map(|x| 1.0 + x).collect::<Vec<_>>();
1267 let ffn_norm = rnd(n_embd, 0.5).iter().map(|x| 1.0 + x).collect::<Vec<_>>();
1268 let (gate_v, up_v, down_v) = (
1269 rnd(ffn_dim * n_embd, 1.0),
1270 rnd(ffn_dim * n_embd, 1.0),
1271 rnd(n_embd * ffn_dim, 1.0),
1272 );
1273 let gate = mat(ffn_dim, n_embd, gate_v.clone());
1274 let up = mat(ffn_dim, n_embd, up_v.clone());
1275 let down = mat(n_embd, ffn_dim, down_v.clone());
1276 let (conv_len, ssm_len) = h.state_floats();
1277 let conv0 = rnd(conv_len, 1.0);
1278 let ssm0 = rnd(ssm_len, 0.5);
1279 let hidden = rnd(n_embd, 1.0);
1280 let eps = 1e-6f32;
1281 let state0 = || RecurrentState {
1282 conv: {
1283 let mut a = frink_core::recurrent_state::AlignedF32::zeros(conv_len);
1284 a.copy_from_slice(&conv0);
1285 a
1286 },
1287 ssm: {
1288 let mut a = frink_core::recurrent_state::AlignedF32::zeros(ssm_len);
1289 a.copy_from_slice(&ssm0);
1290 a
1291 },
1292 };
1293
1294 let mut host_state = state0();
1296 let normed = rms_norm(&hidden, &attn_norm, eps);
1297 let branch = m.forward_rows(&normed, 1, &mut host_state, eps);
1298 let mut host_out = hidden.clone();
1299 for (x, b) in host_out.iter_mut().zip(branch.iter()) {
1300 *x += b;
1301 }
1302 let normed2 = rms_norm(&host_out, &ffn_norm, eps);
1303 let ffn_out = frink_moe::run_expert(
1304 &normed2,
1305 &frink_moe::ExpertWeights {
1306 gate: mat(ffn_dim, n_embd, gate_v),
1307 up: mat(ffn_dim, n_embd, up_v),
1308 down: mat(n_embd, ffn_dim, down_v),
1309 },
1310 frink_moe::GluAct::Swiglu,
1311 );
1312 for (x, f) in host_out.iter_mut().zip(ffn_out.iter()) {
1313 *x += f;
1314 }
1315
1316 let mut device_state = state0();
1318 let ffn = LayerFfnParts::from_parts(&ffn_norm, eps, &gate, &up, &down);
1319 let head = LayerHeadParts::for_block(
1320 &attn_norm,
1321 eps,
1322 &m.qkv,
1323 &m.z_proj,
1324 match &m.beta_alpha {
1325 BetaAlpha::Split { beta, .. } => beta,
1326 BetaAlpha::Fused { .. } => unreachable!("split above"),
1327 },
1328 match &m.beta_alpha {
1329 BetaAlpha::Split { alpha, .. } => alpha,
1330 BetaAlpha::Fused { .. } => unreachable!("split above"),
1331 },
1332 )
1333 .expect("every field is required");
1334 let with_head = m
1337 .device_branch_full(
1338 &mut device_state,
1339 eps,
1340 Some(&head),
1341 Some((&ffn, &hidden)),
1342 None,
1343 )
1344 .expect("this shape is one the kernels serve");
1345 let mut host_head_state = state0();
1346 let host_head = m
1347 .fused_layer(
1348 &attn_norm,
1349 &normed,
1350 &mut host_head_state,
1351 eps,
1352 &ffn,
1353 &hidden,
1354 )
1355 .expect("this shape is one the kernels serve");
1356
1357 let tol = 2e-4;
1358 for (what, got) in [
1359 ("head on device", &with_head),
1360 ("through fused_layer", &host_head),
1361 ] {
1362 for (i, (x, y)) in got.iter().zip(host_out.iter()).enumerate() {
1363 assert!(
1364 (x - y).abs() <= tol * y.abs().max(1.0),
1365 "{what} out[{i}]: device={x} host={y}"
1366 );
1367 }
1368 }
1369 for (i, (x, y)) in device_state
1370 .ssm
1371 .iter()
1372 .zip(host_state.ssm.iter())
1373 .enumerate()
1374 {
1375 assert!(
1376 (x - y).abs() <= tol * y.abs().max(1.0),
1377 "ssm state[{i}]: device={x} host={y}"
1378 );
1379 }
1380 }
1381
1382 #[test]
1385 fn rows_and_one_at_a_time_agree_and_leave_the_same_state() {
1386 let h = hp();
1387 let n_embd = 3;
1388 let mut seed = 5u32;
1389 let mut rnd = |n: usize| -> Vec<f32> {
1390 (0..n)
1391 .map(|_| {
1392 seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
1393 ((seed >> 9) as f32 / (1u32 << 23) as f32) - 0.5
1394 })
1395 .collect()
1396 };
1397 let mat = |rows: usize, cols: usize, v: Vec<f32>| {
1398 WeightMatrix::F32(Tensor::new(v, vec![rows, cols]))
1399 };
1400 let m = Gdn {
1401 h,
1402 qkv: mat(h.conv_dim(), n_embd, rnd(h.conv_dim() * n_embd)),
1403 z_proj: mat(h.value_dim(), n_embd, rnd(h.value_dim() * n_embd)),
1404 conv1d: rnd(h.d_conv * h.conv_dim()),
1405 dt_bias: rnd(h.n_v_heads),
1406 a: vec![-0.7, -1.2],
1407 beta_alpha: BetaAlpha::Split {
1408 beta: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd)),
1409 alpha: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd)),
1410 },
1411 norm: vec![1.1, 0.9],
1412 out_proj: mat(n_embd, h.value_dim(), rnd(n_embd * h.value_dim())),
1413 };
1414 let tokens: Vec<Vec<f32>> = (0..4).map(|_| rnd(n_embd)).collect();
1415 let flat: Vec<f32> = tokens.concat();
1416 let mut s_batch = m.zero_state();
1417 let batched = m.forward_rows(&flat, 4, &mut s_batch, 1e-5);
1418 let mut s_seq = m.zero_state();
1419 let mut seq = Vec::new();
1420 for t in &tokens {
1421 seq.extend(m.forward_rows(t, 1, &mut s_seq, 1e-5));
1422 }
1423 for (a, b) in batched.iter().zip(&seq) {
1424 assert!((a - b).abs() < 1e-6, "{a} vs {b}");
1425 }
1426 assert_eq!(s_batch.conv, s_seq.conv, "the conv window is exact");
1434 for (a, b) in s_batch.ssm.iter().zip(s_seq.ssm.iter()) {
1435 assert!((a - b).abs() < 1e-6, "state: {a} vs {b}");
1436 }
1437 let again = m.forward_rows(&flat, 4, &mut s_batch, 1e-5);
1438 assert!(again
1439 .iter()
1440 .zip(&batched)
1441 .any(|(a, b)| (a - b).abs() > 1e-6));
1442 }
1443}