1use crate::config::ModelConfig;
54use crate::decoder::AttnWeights;
55use crate::loader::{load_weight_matrix, LoadError};
56use crate::norm::NormOp;
57use crate::norm_sites::NormSites;
58use frink_core::cache::{KvCache, PagedKvStore, SharedPagedKv};
59use frink_core::{Tensor, WeightMatrix};
60use frink_gguf::{GgufValue, TensorSource};
61use frink_moe::ExpertWeights;
62
63pub const PER_LAYER_SHAPE_ARCHS: &[(&str, &str)] = &[
74 (
75 "deci",
76 "generic. deci.cpp:30-34 (loader) and :103-105 (graph): all three per layer, with \
77 n_head == 0 an attention-free layer, n_head_kv == 0 a wo-only layer and n_ff == 0 \
78 an FFN-free layer",
79 ),
80 (
81 "openelm",
82 "generic. openelm.cpp:26-28 (loader) and :67-69 (graph): all three per layer, sizing \
83 one fused wqkv per layer",
84 ),
85 (
86 "plamo3",
87 "generic. plamo3.cpp:39-44 (loader) and :110-111 (graph); no published PLaMo-3 \
88 export writes an array (conversion/plamo.py:27-30 writes scalars), so the seam is \
89 latent there",
90 ),
91 (
92 "laguna",
93 "generic. laguna.cpp:87-88 (loader) and :176-177 (graph) read n_head(i) per layer; \
94 KV heads uniform (:86). Closed with the gated attention (`crate::attn_gate`); the \
95 second rotary width at :50 is `ModelConfig::rope_dim_swa` (`crate::swa_geometry`)",
96 ),
97 (
98 "mimo2",
99 "mimo2.cpp:47-49,111-112 read heads per layer (`swa_num_key_value_heads` on the \
100 sliding layers, the converter's array). Closed with the split K/V head width \
101 (`crate::kv_head_dims`, the V width :47-48 sizes apart from K's) and the value \
102 scale (`crate::attn_value_scale`, :16,181); the sinks at :58 are \
103 `AttnWeights::sinks`, the is_swa array at :12 is `crate::swa_layers` and the NEXTN \
104 blocks at :19 are `crate::mtp_blocks`",
105 ),
106 (
107 "step35",
108 "generic. step35.cpp:76-78,208-209 (loader and graph) read heads and KV widths per \
109 layer. Closed with the per-layer activation seam (`crate::act_layers`, the clamp \
110 arrays at :28-29) and the two-valued rotary width (`crate::swa_geometry`, :9); the \
111 gate at :96 is `crate::attn_gate`, the is_swa array at :26 `crate::swa_layers`, \
112 the NEXTN blocks at :32 `crate::mtp_blocks`",
113 ),
114 (
115 "spark2_5",
116 "generic. spark2-5.cpp:33-37 (loader) and :76-77 (graph) read n_head(i) and \
117 n_head_kv(i) per layer, sizing the per-head attention gate (:41) by each layer's \
118 own count. Landed upstream after the 2026-08-04 pin and closed on 2026-09-19 with \
119 one `crate::attn_gate` row",
120 ),
121 (
122 "maple",
123 "generic. maple.cpp:6 reads `expert_feed_forward_length` as an ARRAY at \
124 n_layer_all length; the tensors are sized from n_ff_exp() (layer 0) at :27, so \
125 the array must LOAD even where every entry agrees. Landed upstream after the \
126 2026-08-04 pin and closed on 2026-09-19 with one `crate::rope_layers` row",
127 ),
128 (
129 "nanbeige",
130 "nanbeige.cpp:24-26 copies each physical layer's arrays to every logical slot; \
131 `LayerShapes::replicated` does the same and `crate::layer_loops` is the seam the row \
132 closed on",
133 ),
134 (
135 "gemma4",
136 "dedicated engine. gemma4.cpp:64-67,91 (loader) and :179-184 (graph)",
137 ),
138 (
139 "gemma4-assistant",
140 "dedicated engine. gemma4-assistant.cpp:53-55 (loader) and :134-138 (graph)",
141 ),
142 (
143 "jamba",
144 "generic. jamba.cpp:8-10 (hparams), :37-58 (loader) and :90-92 (graph): n_head_kv(i) \
145 == 0 marks a Mamba-1 layer (`crate::mamba1`), served since 2026-09-14",
146 ),
147 (
148 "lfm2",
149 "hybrid: n_head_kv(il) == 0 marks a recurrent layer (lfm2.cpp:10,72,130-132)",
150 ),
151 (
152 "lfm2moe",
153 "hybrid: n_head_kv(il) == 0 marks a recurrent layer (lfm2moe.cpp:13,63)",
154 ),
155 (
156 "nemotron_h",
157 "generic. nemotron-h.cpp:9-11 (hparams), :53-98 (loader) and :146-153 (graph): \
158 n_head_kv(i) == 0 && n_ff(i) == 0 marks a Mamba-2 layer, n_ff(i) == 0 alone an \
159 attention layer, the rest an FFN-only layer; one block per layer \
160 (`BLOCK_WITHOUT_FFN_KEEPS_ITS_OUTPUT`), served since 2026-09-14",
161 ),
162 (
163 "nemotron_h_moe",
164 "nemotron-h.cpp:9-11, the same rule; its latent ungated ReLU-squared MoE (:79-90) is \
165 not served yet",
166 ),
167 (
168 "plamo2",
169 "hybrid: n_head_kv(i) == 0 marks a recurrent layer (plamo2.cpp:19,82-84,218-219)",
170 ),
171 (
172 "granitehybrid",
173 "generic. granite-hybrid.cpp:17-19 (hparams), :58-77 (loader) and :137-140 (graph): \
174 n_head_kv(i) == 0 marks a Mamba-2 layer (`crate::mamba2`), served since 2026-09-14",
175 ),
176 (
177 "granite-hybrid",
178 "generic. the frink alias of `granitehybrid` (granite-hybrid.cpp:17-19), the same rule",
179 ),
180 (
181 "kimi-linear",
182 "hybrid: n_head_kv(i) == 0 marks a KDA layer (kimi-linear.cpp:18)",
183 ),
184];
185
186pub fn per_layer_shapes_read_by_llama_cpp(arch: &str) -> bool {
188 PER_LAYER_SHAPE_ARCHS.iter().any(|(a, _)| *a == arch)
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum AttnShape {
198 Gqa { n_heads: usize, n_kv_heads: usize },
200 Linear,
205 Absent,
209 ShortConv,
216 Mamba2,
222 Mamba1,
226 Plamo2Ssm,
229 Gdn,
234 Lightning,
238}
239
240pub const BLOCK_WITHOUT_FFN_KEEPS_ITS_OUTPUT: &[&str] =
250 &["nemotron_h", "nemotron_h_moe", "mamba", "mamba2"];
251
252pub const PURE_RECURRENT: &[(&str, ZeroKvLayer)] = &[
259 ("mamba", ZeroKvLayer::Mamba1),
260 ("mamba2", ZeroKvLayer::Mamba2),
261];
262
263pub fn pure_recurrent_block(arch: &str) -> Option<ZeroKvLayer> {
265 PURE_RECURRENT
266 .iter()
267 .find(|(a, _)| *a == arch)
268 .map(|(_, k)| *k)
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum ZeroKvLayer {
276 Linear,
278 ShortConv,
280 Mamba2,
282 Mamba1,
284 Plamo2,
286 Mamba2UnlessFfn,
292 Unserved(&'static str),
294}
295
296impl ZeroKvLayer {
297 pub fn for_arch(arch: &str) -> Self {
303 if crate::shortconv::is_shortconv_architecture(arch) {
304 return ZeroKvLayer::ShortConv;
305 }
306 match arch {
307 "granitehybrid" | "granite-hybrid" => ZeroKvLayer::Mamba2,
310 "jamba" => ZeroKvLayer::Mamba1,
312 "falcon-h1" => ZeroKvLayer::Unserved(
317 "no falcon-h1 layer has a zero KV count: falcon-h1.cpp:137-161 runs attention \
318 AND the Mamba-2 block on every layer (`ModelConfig::parallel_ssm`)",
319 ),
320 "nemotron_h" | "nemotron_h_moe" => ZeroKvLayer::Mamba2UnlessFfn,
326 "plamo2" => ZeroKvLayer::Plamo2,
330 "kimi-linear" => ZeroKvLayer::Unserved(
331 "a KDA block (kimi-linear.cpp:18), served by `crate::kimi_decoder` and not \
332 the generic path",
333 ),
334 _ => ZeroKvLayer::Linear,
335 }
336 }
337}
338
339impl AttnShape {
340 pub fn from_counts(
350 n_heads: usize,
351 n_kv_heads: usize,
352 ffn_dim: usize,
353 zero_kv: ZeroKvLayer,
354 ) -> Result<Self, String> {
355 match (n_heads, n_kv_heads) {
356 (0, 0) if zero_kv == ZeroKvLayer::Plamo2 => Ok(AttnShape::Plamo2Ssm),
361 (0, 0) => Ok(AttnShape::Absent),
362 (0, kv) => Err(format!(
363 "head_count 0 with head_count_kv {kv}: deci.cpp:107 would skip attention while \
364 :44 sizes a zero-wide Q projection"
365 )),
366 (_, 0) => match zero_kv {
367 ZeroKvLayer::Linear => Ok(AttnShape::Linear),
368 ZeroKvLayer::ShortConv => Ok(AttnShape::ShortConv),
369 ZeroKvLayer::Mamba2 => Ok(AttnShape::Mamba2),
370 ZeroKvLayer::Mamba1 => Ok(AttnShape::Mamba1),
371 ZeroKvLayer::Plamo2 => Ok(AttnShape::Plamo2Ssm),
372 ZeroKvLayer::Mamba2UnlessFfn if ffn_dim == 0 => Ok(AttnShape::Mamba2),
374 ZeroKvLayer::Mamba2UnlessFfn => Ok(AttnShape::Absent),
376 ZeroKvLayer::Unserved(what) => Err(format!(
377 "head_count_kv 0 marks {what}; `layer_shapes::ZeroKvLayer` is the table"
378 )),
379 },
380 (q, kv) if q % kv != 0 => Err(format!(
381 "head_count {q} is not a multiple of head_count_kv {kv}"
382 )),
383 (n_heads, n_kv_heads) => Ok(AttnShape::Gqa {
384 n_heads,
385 n_kv_heads,
386 }),
387 }
388 }
389
390 pub fn n_kv_heads(self) -> usize {
393 match self {
394 AttnShape::Gqa { n_kv_heads, .. } => n_kv_heads,
395 AttnShape::Linear
396 | AttnShape::Absent
397 | AttnShape::ShortConv
398 | AttnShape::Mamba2
399 | AttnShape::Mamba1
400 | AttnShape::Plamo2Ssm
401 | AttnShape::Gdn
402 | AttnShape::Lightning => 0,
403 }
404 }
405
406 pub fn n_heads(self) -> usize {
408 match self {
409 AttnShape::Gqa { n_heads, .. } => n_heads,
410 AttnShape::Linear
411 | AttnShape::Absent
412 | AttnShape::ShortConv
413 | AttnShape::Mamba2
414 | AttnShape::Mamba1
415 | AttnShape::Plamo2Ssm
416 | AttnShape::Gdn
417 | AttnShape::Lightning => 0,
418 }
419 }
420
421 pub fn is_recurrent(self) -> bool {
424 matches!(
425 self,
426 AttnShape::Mamba2
427 | AttnShape::Mamba1
428 | AttnShape::Plamo2Ssm
429 | AttnShape::Gdn
430 | AttnShape::Lightning
431 )
432 }
433
434 pub fn cache_geometry(
447 self,
448 head_dim: usize,
449 v_head_dim: usize,
450 hidden_dim: usize,
451 ) -> (usize, usize, usize) {
452 match self {
453 AttnShape::Gqa { n_kv_heads, .. } => (n_kv_heads, head_dim, v_head_dim),
454 AttnShape::Linear
455 | AttnShape::Absent
456 | AttnShape::Mamba2
457 | AttnShape::Mamba1
458 | AttnShape::Plamo2Ssm
459 | AttnShape::Gdn
460 | AttnShape::Lightning => (0, head_dim, v_head_dim),
461 AttnShape::ShortConv => (1, hidden_dim, 0),
462 }
463 }
464}
465
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
468pub struct LayerShape {
469 pub attention: AttnShape,
470 pub ffn_dim: usize,
473}
474
475#[derive(Debug, Clone, PartialEq, Eq, Default)]
482pub enum LayerShapes {
483 #[default]
486 Uniform,
487 PerLayer(Vec<LayerShape>),
489}
490
491impl LayerShapes {
492 pub fn is_uniform(&self) -> bool {
493 matches!(self, LayerShapes::Uniform)
494 }
495
496 pub fn replicated(self, n_loops: usize) -> Self {
500 match self {
501 LayerShapes::PerLayer(v) if n_loops > 1 => {
502 LayerShapes::PerLayer(v.iter().copied().cycle().take(v.len() * n_loops).collect())
503 }
504 other => other,
505 }
506 }
507
508 pub fn resolve(
520 arch: &str,
521 heads: &[u64],
522 kv_heads: &[u64],
523 ffn: Option<&[u64]>,
524 expert_ffn_dim: usize,
525 recurrent: Option<&crate::gdn::RecurrentMask>,
526 ) -> Result<Self, LoadError> {
527 let n = heads.len();
528 assert_eq!(kv_heads.len(), n);
529 if let Some(recurrent) = recurrent {
530 assert_eq!(recurrent.layers.len(), n);
531 let zero_kv = ZeroKvLayer::for_arch(arch);
532 let mut shapes = Vec::with_capacity(n);
533 for il in 0..n {
534 let ffn_dim = ffn.map_or(expert_ffn_dim, |f| f[il] as usize);
535 let attention = if recurrent.layers[il] {
536 recurrent.block
537 } else {
538 AttnShape::from_counts(
539 heads[il] as usize,
540 kv_heads[il] as usize,
541 ffn_dim,
542 zero_kv,
543 )
544 .map_err(|why| {
545 LoadError::UnsupportedFeature(arch.to_string(), format!("blk.{il}: {why}"))
546 })?
547 };
548 shapes.push(LayerShape { attention, ffn_dim });
549 }
550 return Ok(LayerShapes::PerLayer(shapes));
551 }
552 if let Some(kind) = pure_recurrent_block(arch) {
556 let shape = AttnShape::from_counts(1, 0, 0, kind)
557 .map_err(|why| LoadError::UnsupportedFeature(arch.to_string(), why))?;
558 for il in 0..n {
559 let ffn_dim = ffn.map_or(0, |f| f[il] as usize);
560 if heads[il] != 0 || kv_heads[il] != 0 || ffn_dim != 0 {
561 return Err(LoadError::UnsupportedFeature(
562 arch.to_string(),
563 format!(
564 "blk.{il}: head_count {} / head_count_kv {} / feed_forward_length \
565 {ffn_dim} on a pure recurrent architecture, whose converter writes \
566 0 for all three (conversion/mamba.py:155-156) and whose graph has no \
567 attention and no FFN (mamba.cpp:73-88)",
568 heads[il], kv_heads[il]
569 ),
570 ));
571 }
572 }
573 return Ok(LayerShapes::PerLayer(vec![
574 LayerShape {
575 attention: shape,
576 ffn_dim: 0
577 };
578 n
579 ]));
580 }
581 let uniform = heads.windows(2).all(|w| w[0] == w[1])
582 && kv_heads.windows(2).all(|w| w[0] == w[1])
583 && ffn.is_none_or(|f| f.windows(2).all(|w| w[0] == w[1]));
584 if uniform {
585 return Ok(LayerShapes::Uniform);
586 }
587 if !per_layer_shapes_read_by_llama_cpp(arch) {
588 return Err(LoadError::UnsupportedFeature(
589 arch.to_string(),
590 format!(
591 "per-layer head_count / head_count_kv / feed_forward_length arrays whose \
592 entries differ (heads {heads:?}, kv {kv_heads:?}, ff {ffn:?}). llama.cpp \
593 reads these arrays for every architecture (llama-model.cpp:1149-1158) but \
594 this one's graph takes layer 0 through LLAMA_LOAD_LOCALS \
595 (llama-model.h:760-767), so such a file cannot load there either; \
596 `layer_shapes::PER_LAYER_SHAPE_ARCHS` lists the ones that index per layer"
597 ),
598 ));
599 }
600 let mut shapes = Vec::with_capacity(n);
601 let zero_kv = ZeroKvLayer::for_arch(arch);
602 let keeps_output = BLOCK_WITHOUT_FFN_KEEPS_ITS_OUTPUT.contains(&arch);
603 for il in 0..n {
604 let ffn_dim = ffn.map_or(expert_ffn_dim, |f| f[il] as usize);
605 let attention =
606 AttnShape::from_counts(heads[il] as usize, kv_heads[il] as usize, ffn_dim, zero_kv)
607 .map_err(|why| {
608 LoadError::UnsupportedFeature(arch.to_string(), format!("blk.{il}: {why}"))
609 })?;
610 if ffn_dim == 0 && attention != AttnShape::Absent && !keeps_output {
611 return Err(LoadError::UnsupportedFeature(
622 arch.to_string(),
623 format!(
624 "blk.{il}: feed_forward_length 0 on a layer WITH attention \
625 (head_count {}). deci.cpp:147-149 `continue`s before the residual \
626 add at :150-153, discarding the attention output that :115-137 \
627 computed, and frink will not reproduce a dropped branch as the \
628 reference. An FFN-free layer with head_count 0 is supported",
629 heads[il]
630 ),
631 ));
632 }
633 shapes.push(LayerShape { attention, ffn_dim });
634 }
635 Ok(LayerShapes::PerLayer(shapes))
636 }
637}
638
639pub fn read_u64_trunk_layers(
659 file: &impl TensorSource,
660 key: &str,
661 trunk: &crate::mtp_blocks::TrunkLayers,
662) -> Result<Option<Vec<u64>>, LoadError> {
663 Ok(
664 read_u64_per_layer(file, key, trunk.block_count)?.map(|mut v| {
665 v.truncate(trunk.n_layers);
666 v
667 }),
668 )
669}
670
671pub fn read_u64_per_layer(
672 file: &impl TensorSource,
673 key: &str,
674 n_layers: usize,
675) -> Result<Option<Vec<u64>>, LoadError> {
676 let Some(value) = file.metadata(key) else {
677 return Ok(None);
678 };
679 match value {
680 GgufValue::Array(items) => {
681 if items.len() != n_layers {
682 return Err(LoadError::UnsupportedFeature(
683 key.to_string(),
684 format!(
685 "array of {} entries for {n_layers} layers; llama.cpp refuses this too \
686 (`key has wrong array length`, llama-model-loader.cpp:464-465)",
687 items.len()
688 ),
689 ));
690 }
691 let mut out = Vec::with_capacity(n_layers);
692 for (il, item) in items.iter().enumerate() {
693 out.push(item.as_u64().ok_or_else(|| {
694 LoadError::UnsupportedFeature(
695 key.to_string(),
696 format!("entry {il} is not an unsigned integer: {item:?}"),
697 )
698 })?);
699 }
700 Ok(Some(out))
701 }
702 scalar => scalar
703 .as_u64()
704 .map(|v| Some(vec![v; n_layers]))
705 .ok_or_else(|| LoadError::MissingHparam(key.to_string())),
706 }
707}
708
709fn no_rows(cols: usize) -> WeightMatrix {
720 WeightMatrix::F32(Tensor::new(Vec::new(), vec![0, cols]))
721}
722
723pub(crate) fn load_non_gqa_attention(
733 shape: AttnShape,
734 file: &impl TensorSource,
735 arch: &str,
736 layer: usize,
737 norm_sites: &NormSites,
738 config: &ModelConfig,
739) -> Result<AttnWeights, LoadError> {
740 let hidden_dim = config.hidden_dim;
741 let mut shortconv = None;
742 let mut ssm = None;
743 let (norm_weight, o_proj) = match shape {
744 AttnShape::Linear => (
745 norm_sites.load_pre_norm(norm_sites.attn, file, Some(layer))?,
746 load_weight_matrix(file, &format!("blk.{layer}.attn_output.weight"))?,
747 ),
748 AttnShape::Absent => (NormOp::None, no_rows(0)),
749 AttnShape::ShortConv => {
750 shortconv = Some(crate::shortconv::ShortConv::load(
751 file, arch, layer, hidden_dim,
752 )?);
753 (
754 norm_sites.load_pre_norm(norm_sites.attn, file, Some(layer))?,
755 no_rows(0),
756 )
757 }
758 AttnShape::Mamba2 => {
759 ssm = Some(crate::ssm_block::SsmBlock::Mamba2(
760 crate::mamba2::Mamba2::load(file, arch, layer, hidden_dim)?,
761 ));
762 (
763 norm_sites.load_pre_norm(norm_sites.attn, file, Some(layer))?,
764 no_rows(0),
765 )
766 }
767 AttnShape::Mamba1 => {
768 ssm = Some(crate::ssm_block::SsmBlock::Mamba1(
769 crate::mamba1::Mamba1::load(file, arch, layer, hidden_dim)?,
770 ));
771 (
772 norm_sites.load_pre_norm(norm_sites.attn, file, Some(layer))?,
773 no_rows(0),
774 )
775 }
776 AttnShape::Plamo2Ssm => {
777 ssm = Some(crate::ssm_block::SsmBlock::Plamo2(
778 crate::plamo2_ssm::Plamo2Ssm::load(file, arch, layer, hidden_dim)?,
779 ));
780 (
781 norm_sites.load_pre_norm(norm_sites.attn, file, Some(layer))?,
782 no_rows(0),
783 )
784 }
785 AttnShape::Gdn => {
786 ssm = Some(crate::ssm_block::SsmBlock::Gdn(crate::gdn::Gdn::load(
787 file, arch, layer, hidden_dim,
788 )?));
789 (
790 norm_sites.load_pre_norm(norm_sites.attn, file, Some(layer))?,
791 no_rows(0),
792 )
793 }
794 AttnShape::Lightning => {
795 ssm = Some(crate::ssm_block::SsmBlock::Lightning(
796 crate::lightning::Lightning::load(
797 file,
798 layer,
799 config.n_layers,
800 config.n_heads,
801 config.head_dim,
802 hidden_dim,
803 )?,
804 ));
805 (
806 norm_sites.load_pre_norm(norm_sites.attn, file, Some(layer))?,
807 no_rows(0),
808 )
809 }
810 AttnShape::Gqa { .. } => unreachable!("a GQA layer loads its projections"),
811 };
812 if let AttnShape::Linear = shape {
813 if o_proj.rows() != hidden_dim || o_proj.cols() != hidden_dim {
814 return Err(LoadError::UnsupportedFeature(
815 format!("blk.{layer}.attn_output.weight"),
816 format!(
817 "a wo-only layer's projection is {{n_embd, n_embd}} (deci.cpp:39); this one \
818 is {}x{} for hidden_dim {hidden_dim}",
819 o_proj.rows(),
820 o_proj.cols()
821 ),
822 ));
823 }
824 }
825 Ok(AttnWeights {
826 q_proj: no_rows(hidden_dim),
827 k_proj: no_rows(hidden_dim),
828 v_proj: no_rows(hidden_dim),
829 o_proj,
830 norm_weight,
831 q_norm: None,
832 k_norm: None,
833 q_bias: None,
834 k_bias: None,
835 v_bias: None,
836 post_attn_norm: match shape {
842 AttnShape::Plamo2Ssm => NormSites::load_post_norm(norm_sites.post_attn, file, layer)?,
843 _ => None,
844 },
845 post_ffn_norm: NormSites::load_post_norm(norm_sites.post_ffn, file, layer)?,
848 output_gate: None,
849 sinks: None,
850 attn_sub_norm: None,
853 o_scale: None,
854 o_bias: None,
855 shortconv,
856 ssm,
857 q_gate_interleaved: false,
858 })
859}
860
861pub(crate) fn absent_ffn(hidden_dim: usize) -> ExpertWeights {
871 ExpertWeights {
872 gate: no_rows(hidden_dim),
873 up: no_rows(hidden_dim),
874 down: no_rows(0),
875 }
876}
877
878pub(crate) fn check_gqa_projection_widths(
885 layer: usize,
886 shape: AttnShape,
887 head_dim: usize,
888 v_head_dim: usize,
889 hidden_dim: usize,
890 attn: &AttnWeights,
891) -> Result<(), LoadError> {
892 let AttnShape::Gqa {
893 n_heads,
894 n_kv_heads,
895 } = shape
896 else {
897 unreachable!("only GQA layers have Q/K/V to check")
898 };
899 let q_rows = if attn.q_gate_interleaved { 2 } else { 1 } * n_heads * head_dim;
902 let want = [
903 ("attn_q", attn.q_proj.rows(), q_rows),
904 ("attn_k", attn.k_proj.rows(), n_kv_heads * head_dim),
905 ("attn_v", attn.v_proj.rows(), n_kv_heads * v_head_dim),
909 ("attn_output (rows)", attn.o_proj.rows(), hidden_dim),
910 (
911 "attn_output (cols)",
912 attn.o_proj.cols(),
913 n_heads * v_head_dim,
914 ),
915 ];
916 for (name, got, expected) in want {
917 if got != expected {
918 return Err(LoadError::UnsupportedFeature(
919 format!("blk.{layer}.{name}.weight"),
920 format!(
921 "{got} does not match this layer's head_count {n_heads} / head_count_kv \
922 {n_kv_heads} x head_dim {head_dim} / v_head_dim {v_head_dim} (expected \
923 {expected}); llama.cpp's check_tensor_dims refuses the same file"
924 ),
925 ));
926 }
927 }
928 Ok(())
929}
930
931impl ModelConfig {
932 pub fn has_recurrent_layers(&self) -> bool {
938 self.parallel_ssm
939 || (0..self.n_layers).any(|il| self.layer_shape(il).attention.is_recurrent())
940 }
941
942 pub fn layer_cache_geometry(&self, il: usize) -> (usize, usize, usize) {
945 self.layer_shape(il).attention.cache_geometry(
946 self.head_dim,
947 self.v_head_dim(),
948 self.hidden_dim,
949 )
950 }
951
952 pub fn layer_shape(&self, il: usize) -> LayerShape {
955 match &self.layer_shapes {
956 LayerShapes::Uniform => LayerShape {
957 attention: AttnShape::Gqa {
958 n_heads: self.n_heads,
959 n_kv_heads: self.n_kv_heads,
960 },
961 ffn_dim: self.moe.expert_ffn_dim,
962 },
963 LayerShapes::PerLayer(v) => v[il],
964 }
965 }
966
967 pub fn new_kv_caches(&self) -> Vec<KvCache> {
974 (0..self.n_layers)
975 .map(|il| {
976 let (n_kv_heads, head_dim, v_head_dim) = self.layer_cache_geometry(il);
977 KvCache::new_split(n_kv_heads, head_dim, v_head_dim)
978 })
979 .collect()
980 }
981
982 pub fn new_kv_caches_with_capacity(&self, max_seq_len: usize) -> Vec<KvCache> {
984 (0..self.n_layers)
985 .map(|il| {
986 let (n_kv_heads, head_dim, v_head_dim) = self.layer_cache_geometry(il);
987 KvCache::with_capacity_split(n_kv_heads, head_dim, v_head_dim, max_seq_len)
988 })
989 .collect()
990 }
991
992 pub fn new_kv_caches_with_pool(
995 &self,
996 pool: &std::sync::Arc<std::sync::Mutex<frink_core::cache::KvBlockPool>>,
997 max_seq_len: usize,
998 ) -> Result<Vec<KvCache>, frink_core::cache::KvPoolExhausted> {
999 (0..self.n_layers)
1000 .map(|il| {
1001 let (n_kv_heads, head_dim, v_head_dim) = self.layer_cache_geometry(il);
1002 KvCache::with_pool_split(
1003 n_kv_heads,
1004 head_dim,
1005 v_head_dim,
1006 std::sync::Arc::clone(pool),
1007 max_seq_len,
1008 )
1009 })
1010 .collect()
1011 }
1012
1013 pub fn new_paged_kv(&self, block_size: usize, blocks_per_layer: usize) -> SharedPagedKv {
1015 SharedPagedKv::from_stores(
1016 (0..self.n_layers)
1017 .map(|il| {
1018 let (n_kv_heads, head_dim, v_head_dim) = self.layer_cache_geometry(il);
1019 PagedKvStore::new_split(
1020 block_size,
1021 blocks_per_layer,
1022 n_kv_heads,
1023 head_dim,
1024 v_head_dim,
1025 )
1026 })
1027 .collect(),
1028 )
1029 }
1030
1031 pub fn kv_heads_all_layers(&self) -> usize {
1036 (0..self.n_layers)
1037 .map(|il| self.layer_shape(il).attention.n_kv_heads())
1038 .sum()
1039 }
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044 use super::*;
1045
1046 fn deci_like() -> Vec<LayerShape> {
1047 vec![
1048 LayerShape {
1049 attention: AttnShape::Gqa {
1050 n_heads: 4,
1051 n_kv_heads: 2,
1052 },
1053 ffn_dim: 16,
1054 },
1055 LayerShape {
1056 attention: AttnShape::Linear,
1057 ffn_dim: 8,
1058 },
1059 LayerShape {
1060 attention: AttnShape::Absent,
1061 ffn_dim: 16,
1062 },
1063 LayerShape {
1064 attention: AttnShape::Absent,
1065 ffn_dim: 0,
1066 },
1067 ]
1068 }
1069
1070 #[test]
1072 fn the_two_zero_counts_are_two_different_layer_kinds() {
1073 let deci = ZeroKvLayer::for_arch("deci");
1074 assert_eq!(
1075 AttnShape::from_counts(0, 0, 16, deci),
1076 Ok(AttnShape::Absent)
1077 );
1078 assert_eq!(
1079 AttnShape::from_counts(4, 0, 16, deci),
1080 Ok(AttnShape::Linear)
1081 );
1082 assert_eq!(
1083 AttnShape::from_counts(4, 2, 16, deci),
1084 Ok(AttnShape::Gqa {
1085 n_heads: 4,
1086 n_kv_heads: 2
1087 })
1088 );
1089 assert!(AttnShape::from_counts(0, 2, 16, deci).is_err());
1090 assert!(AttnShape::from_counts(3, 2, 16, deci).is_err());
1091 assert_eq!(AttnShape::Linear.n_kv_heads(), 0);
1092 assert_eq!(AttnShape::Absent.n_heads(), 0);
1093 }
1094
1095 #[test]
1099 fn a_zero_kv_layer_means_what_the_architecture_says() {
1100 let lfm2 = ZeroKvLayer::for_arch("lfm2");
1101 assert_eq!(
1102 AttnShape::from_counts(4, 0, 16, lfm2),
1103 Ok(AttnShape::ShortConv)
1104 );
1105 assert!(matches!(
1107 AttnShape::from_counts(4, 2, 16, lfm2),
1108 Ok(AttnShape::Gqa { .. })
1109 ));
1110 assert_eq!(
1111 AttnShape::from_counts(4, 0, 16, ZeroKvLayer::for_arch("jamba")),
1112 Ok(AttnShape::Mamba1)
1113 );
1114 assert_eq!(
1120 AttnShape::from_counts(4, 0, 16, ZeroKvLayer::for_arch("plamo2")),
1121 Ok(AttnShape::Plamo2Ssm)
1122 );
1123 assert_eq!(
1124 AttnShape::from_counts(0, 0, 16, ZeroKvLayer::for_arch("plamo2")),
1125 Ok(AttnShape::Plamo2Ssm)
1126 );
1127 assert_eq!(
1128 AttnShape::from_counts(0, 0, 16, ZeroKvLayer::for_arch("jamba")),
1129 Ok(AttnShape::Absent)
1130 );
1131 let s = LayerShapes::resolve("mamba", &[0, 0], &[0, 0], Some(&[0, 0]), 0, None).unwrap();
1134 let LayerShapes::PerLayer(v) = s else {
1135 panic!("per layer");
1136 };
1137 assert!(v
1138 .iter()
1139 .all(|l| l.attention == AttnShape::Mamba1 && l.ffn_dim == 0));
1140 assert!(
1141 LayerShapes::resolve("mamba2", &[0, 0], &[0, 0], None, 0, None)
1142 .is_ok_and(|s| matches!(s, LayerShapes::PerLayer(_)))
1143 );
1144 assert!(LayerShapes::resolve("mamba", &[4, 4], &[0, 0], None, 0, None).is_err());
1145 assert_eq!(AttnShape::ShortConv.cache_geometry(6, 6, 24), (1, 24, 0));
1147 assert_eq!(
1149 AttnShape::from_counts(4, 0, 16, ZeroKvLayer::for_arch("granitehybrid")),
1150 Ok(AttnShape::Mamba2)
1151 );
1152 let nh = ZeroKvLayer::for_arch("nemotron_h");
1154 assert_eq!(AttnShape::from_counts(4, 0, 0, nh), Ok(AttnShape::Mamba2));
1155 assert_eq!(AttnShape::from_counts(4, 0, 40, nh), Ok(AttnShape::Absent));
1156 assert!(LayerShapes::resolve("deci", &[4, 4], &[2, 2], Some(&[16, 0]), 16, None).is_err());
1159 let s = LayerShapes::resolve(
1160 "nemotron_h",
1161 &[4, 4, 4],
1162 &[0, 2, 0],
1163 Some(&[0, 0, 40]),
1164 16,
1165 None,
1166 )
1167 .unwrap();
1168 let LayerShapes::PerLayer(v) = s else {
1169 panic!("per layer");
1170 };
1171 assert_eq!(
1172 v.iter().map(|l| l.attention).collect::<Vec<_>>(),
1173 [
1174 AttnShape::Mamba2,
1175 AttnShape::Gqa {
1176 n_heads: 4,
1177 n_kv_heads: 2
1178 },
1179 AttnShape::Absent
1180 ]
1181 );
1182 assert_eq!(v.iter().map(|l| l.ffn_dim).collect::<Vec<_>>(), [0, 0, 40]);
1183 assert_eq!(AttnShape::Mamba2.cache_geometry(6, 6, 24), (0, 6, 6));
1184 assert!(AttnShape::Mamba2.is_recurrent() && !AttnShape::ShortConv.is_recurrent());
1185 assert_eq!(AttnShape::Linear.cache_geometry(6, 6, 24), (0, 6, 6));
1186 assert_eq!(AttnShape::ShortConv.n_kv_heads(), 0);
1187 let s = LayerShapes::resolve("plamo2", &[4, 4], &[2, 0], None, 16, None).unwrap();
1188 let LayerShapes::PerLayer(v) = s else {
1189 panic!("per layer");
1190 };
1191 assert_eq!(v[1].attention, AttnShape::Plamo2Ssm);
1192 assert!(AttnShape::Plamo2Ssm.is_recurrent());
1193 assert_eq!(AttnShape::Plamo2Ssm.cache_geometry(8, 8, 32), (0, 8, 8));
1194 let s = LayerShapes::resolve("lfm2", &[4, 4], &[0, 2], None, 16, None).unwrap();
1195 let LayerShapes::PerLayer(v) = s else {
1196 panic!("per layer");
1197 };
1198 assert_eq!(v[0].attention, AttnShape::ShortConv);
1199 }
1200
1201 #[test]
1204 fn equal_arrays_collapse_to_uniform_even_for_a_layer_zero_architecture() {
1205 let s = LayerShapes::resolve("llama", &[4, 4], &[2, 2], Some(&[16, 16]), 16, None).unwrap();
1206 assert!(s.is_uniform());
1207 }
1208
1209 #[test]
1213 fn a_varying_array_is_refused_unless_llama_cpp_indexes_it_per_layer() {
1214 let err = LayerShapes::resolve("llama", &[4, 4], &[2, 1], None, 16, None).unwrap_err();
1215 assert!(format!("{err}").contains("PER_LAYER_SHAPE_ARCHS"), "{err}");
1216 let s = LayerShapes::resolve(
1217 "deci",
1218 &[4, 4, 0, 0],
1219 &[2, 0, 0, 0],
1220 Some(&[16, 8, 16, 0]),
1221 16,
1222 None,
1223 )
1224 .unwrap();
1225 assert_eq!(s, LayerShapes::PerLayer(deci_like()));
1226 }
1227
1228 #[test]
1231 fn an_ffn_free_layer_with_attention_is_refused_and_one_without_is_not() {
1232 let err =
1233 LayerShapes::resolve("deci", &[4, 4], &[2, 2], Some(&[16, 0]), 16, None).unwrap_err();
1234 let msg = format!("{err}");
1235 assert!(msg.contains("deci.cpp:147-149"), "{msg}");
1236 assert!(msg.contains("blk.1"), "{msg}");
1237 assert!(LayerShapes::resolve("deci", &[4, 0], &[2, 0], Some(&[16, 0]), 16, None).is_ok());
1238 }
1239
1240 #[test]
1242 fn caches_are_sized_per_layer_and_the_scalar_is_never_consulted() {
1243 let mut cfg = crate::config::glm_5_2();
1244 cfg.n_layers = 4;
1245 cfg.n_heads = 4;
1246 cfg.n_kv_heads = 2;
1247 cfg.head_dim = 8;
1248 cfg.layer_shapes = LayerShapes::PerLayer(deci_like());
1249 let caches = cfg.new_kv_caches();
1250 assert_eq!(
1251 caches.iter().map(|c| c.n_kv_heads).collect::<Vec<_>>(),
1252 vec![2, 0, 0, 0]
1253 );
1254 assert_eq!(cfg.kv_heads_all_layers(), 2);
1255 assert_eq!(cfg.layer_shape(1).attention, AttnShape::Linear);
1256 assert_eq!(cfg.layer_shape(3).ffn_dim, 0);
1257 let mut wrong = KvCache::new(cfg.n_kv_heads, cfg.head_dim);
1261 let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1262 wrong.push(&[], &[]).unwrap();
1263 }));
1264 assert!(res.is_err(), "push must assert the row width");
1265 cfg.layer_shapes = LayerShapes::Uniform;
1266 assert!(cfg
1267 .new_kv_caches()
1268 .iter()
1269 .all(|c| c.n_kv_heads == 2 && c.head_dim == 8));
1270 assert_eq!(cfg.kv_heads_all_layers(), 8);
1271 }
1272
1273 #[test]
1280 fn a_projection_sized_for_another_layer_s_counts_is_refused_naming_the_tensor() {
1281 let m = |rows: usize, cols: usize| {
1282 WeightMatrix::F32(Tensor::new(vec![0.0; rows * cols], vec![rows, cols]))
1283 };
1284 let shape = AttnShape::Gqa {
1285 n_heads: 4,
1286 n_kv_heads: 2,
1287 };
1288 let (head_dim, hidden) = (6, 24);
1289 let build = |q_rows: usize, k_rows: usize| AttnWeights {
1290 q_proj: m(q_rows, hidden),
1291 k_proj: m(k_rows, hidden),
1292 v_proj: m(k_rows, hidden),
1293 o_proj: m(hidden, q_rows),
1294 norm_weight: NormOp::None,
1295 q_norm: None,
1296 k_norm: None,
1297 q_bias: None,
1298 k_bias: None,
1299 v_bias: None,
1300 post_attn_norm: None,
1301 post_ffn_norm: None,
1302 output_gate: None,
1303 sinks: None,
1304 attn_sub_norm: None,
1305 o_scale: None,
1306 o_bias: None,
1307 shortconv: None,
1308 ssm: None,
1309 q_gate_interleaved: false,
1310 };
1311 assert!(
1312 check_gqa_projection_widths(0, shape, head_dim, head_dim, hidden, &build(24, 12))
1313 .is_ok()
1314 );
1315 let err = check_gqa_projection_widths(1, shape, head_dim, head_dim, hidden, &build(24, 18))
1317 .unwrap_err();
1318 let msg = format!("{err}");
1319 assert!(msg.contains("blk.1.attn_k.weight"), "{msg}");
1320 assert!(msg.contains("head_count_kv 2"), "{msg}");
1321 let err = check_gqa_projection_widths(2, shape, head_dim, head_dim, hidden, &build(18, 12))
1323 .unwrap_err();
1324 assert!(format!("{err}").contains("blk.2.attn_q.weight"), "{err}");
1325 }
1326
1327 #[test]
1330 fn the_reach_table_cites_its_lines_and_names_what_each_row_still_needs() {
1331 for (arch, note) in PER_LAYER_SHAPE_ARCHS {
1332 assert!(note.contains(".cpp:"), "`{arch}` cites no line: {note}");
1333 }
1334 let generic: Vec<&str> = PER_LAYER_SHAPE_ARCHS
1335 .iter()
1336 .filter(|(_, n)| n.starts_with("generic"))
1337 .map(|(a, _)| *a)
1338 .collect();
1339 assert_eq!(
1340 generic,
1341 [
1342 "deci",
1343 "openelm",
1344 "plamo3",
1345 "laguna",
1346 "step35",
1347 "spark2_5",
1348 "maple",
1349 "jamba",
1350 "nemotron_h",
1351 "granitehybrid",
1352 "granite-hybrid"
1353 ]
1354 );
1355 assert!(per_layer_shapes_read_by_llama_cpp("deci"));
1356 assert!(!per_layer_shapes_read_by_llama_cpp("granite"));
1357 }
1358}