1use anyhow::{Result, bail};
30use serde::{Deserialize, Serialize};
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct TensorEntry {
35 pub name: String,
36 pub dims: Vec<usize>,
38 pub bytes: u64,
40}
41
42impl TensorEntry {
43 pub fn new(name: impl Into<String>, dims: Vec<usize>, bytes: u64) -> Self {
44 Self {
45 name: name.into(),
46 dims,
47 bytes,
48 }
49 }
50 pub fn elements(&self) -> u64 {
51 self.dims.iter().product::<usize>() as u64
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct CostOptions {
58 pub block_prefixes: Markers,
60 pub expert_markers: Markers,
66 pub embed_markers: Markers,
68 pub head_markers: Markers,
70 pub expert_active_fraction: f64,
75 pub per_layer_flops: f64,
77 pub kv: KvMarkers,
79 pub kv_dtype: String,
81}
82
83#[derive(Debug, Clone, Default, PartialEq, Eq)]
89pub struct Markers(Vec<String>);
90
91impl Markers {
92 pub fn new<I, S>(names: I) -> Self
93 where
94 I: IntoIterator<Item = S>,
95 S: Into<String>,
96 {
97 Self(names.into_iter().map(Into::into).collect())
98 }
99 pub fn is_empty(&self) -> bool {
100 self.0.is_empty()
101 }
102 pub fn names(&self) -> &[String] {
103 &self.0
104 }
105 pub fn matches(&self, name: &str) -> bool {
107 self.0.iter().any(|p| name.contains(p.as_str()))
108 }
109 pub fn find_in<'a>(&self, ts: &[&'a TensorEntry]) -> Option<&'a TensorEntry> {
111 ts.iter().copied().find(|t| self.matches(&t.name))
112 }
113 pub fn any_in(&self, ts: &[&TensorEntry]) -> bool {
115 ts.iter().any(|t| self.matches(&t.name))
116 }
117 pub fn rank1_len(&self, t: &TensorEntry) -> Option<usize> {
120 match t.dims.as_slice() {
121 [n] if self.matches(&t.name) => Some(*n),
122 _ => None,
123 }
124 }
125}
126
127impl<S: Into<String>> FromIterator<S> for Markers {
128 fn from_iter<I: IntoIterator<Item = S>>(iter: I) -> Self {
129 Self::new(iter)
130 }
131}
132
133#[derive(Debug, Clone)]
140pub struct KvMarkers {
141 pub hidden_probe: Markers,
143 pub recurrent: Markers,
146 pub recurrent_heads: Markers,
148 pub recurrent_head_dim: Markers,
149 pub latent_kv: Markers,
151 pub attn_k: Markers,
154}
155
156impl Default for KvMarkers {
157 fn default() -> Self {
158 Self {
159 hidden_probe: Markers::new(["attn_norm.weight", "input_layernorm.weight"]),
160 recurrent: Markers::new(["ssm_", ".mamba", "conv1d"]),
161 recurrent_heads: Markers::new(["ssm_a", "A_log"]),
162 recurrent_head_dim: Markers::new(["ssm_norm.weight", "o_norm.weight"]),
163 latent_kv: Markers::new(["attn_kv_a_mqa", "kv_a_proj_with_mqa"]),
164 attn_k: Markers::new(["attn_k.weight", "k_proj.weight"]),
165 }
166 }
167}
168
169impl Default for CostOptions {
170 fn default() -> Self {
171 Self {
172 block_prefixes: Markers::new(["blk.", "model.layers.", "layers."]),
173 expert_markers: Markers::new(["_exps", ".experts."]),
174 embed_markers: Markers::new(["token_embd", "embed_tokens", "word_embeddings"]),
175 head_markers: Markers::new(["output.weight", "output_norm", "lm_head", "model.norm"]),
176 expert_active_fraction: 1.0,
177 per_layer_flops: 1.0,
178 kv: KvMarkers::default(),
179 kv_dtype: "f32".into(),
180 }
181 }
182}
183
184impl CostOptions {
185 pub fn with_experts(mut self, experts_per_tok: usize, n_routed: usize) -> Self {
187 if n_routed > 0 {
188 self.expert_active_fraction = experts_per_tok as f64 / n_routed as f64;
189 }
190 self
191 }
192
193 fn block_of(&self, name: &str) -> Option<usize> {
195 for p in self.block_prefixes.names() {
196 if let Some(rest) = name.strip_prefix(p.as_str())
197 && let Some(idx) = rest.split('.').next()
198 && let Ok(i) = idx.parse::<usize>()
199 {
200 return Some(i);
201 }
202 }
203 None
204 }
205 fn is_expert(&self, name: &str) -> bool {
206 self.expert_markers.matches(name)
207 }
208}
209
210pub fn kv_elem_bytes(dtype: &str) -> u64 {
214 match dtype {
215 "f16" | "bf16" | "half" => 2,
216 "f8" | "fp8" | "int8" | "i8" => 1,
217 _ => 4,
218 }
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum BlockKv {
228 Recurrent { state_bytes: u64 },
230 Latent { bytes_per_token: u64 },
233 Attention { bytes_per_token: u64 },
235}
236
237impl BlockKv {
238 pub fn bytes_per_token(&self) -> u64 {
239 match *self {
240 Self::Recurrent { .. } => 0,
241 Self::Latent { bytes_per_token } | Self::Attention { bytes_per_token } => {
242 bytes_per_token
243 }
244 }
245 }
246 pub fn state_bytes(&self) -> u64 {
247 match *self {
248 Self::Recurrent { state_bytes } => state_bytes,
249 _ => 0,
250 }
251 }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
261pub enum KvDetail {
262 PerBlock(Vec<BlockKv>),
264 Flat { bytes_per_layer_token: u64 },
266}
267
268impl Default for KvDetail {
269 fn default() -> Self {
270 Self::Flat {
271 bytes_per_layer_token: 0,
272 }
273 }
274}
275
276fn infer_hidden(entries: &[TensorEntry], opts: &CostOptions) -> usize {
278 entries
279 .iter()
280 .filter(|t| opts.block_of(&t.name).is_some())
281 .filter_map(|t| opts.kv.hidden_probe.rank1_len(t))
282 .max()
283 .unwrap_or(0)
284}
285
286fn classify_block(ts: &[&TensorEntry], m: &KvMarkers, elem: u64) -> Option<BlockKv> {
293 let hidden = ts.iter().find_map(|t| m.hidden_probe.rank1_len(t));
296
297 let out_of = |t: &TensorEntry| -> Option<usize> {
299 let h = hidden?;
300 match t.dims.as_slice() {
301 [a, b] if *a == h && *b != h => Some(*b),
302 [a, b] if *b == h && *a != h => Some(*a),
303 _ => None,
305 }
306 };
307
308 if m.recurrent.any_in(ts) {
311 let heads = ts.iter().find_map(|t| m.recurrent_heads.rank1_len(t))?;
312 let head_dim = ts.iter().find_map(|t| m.recurrent_head_dim.rank1_len(t))?;
313 return Some(BlockKv::Recurrent {
314 state_bytes: (heads * head_dim * head_dim) as u64 * elem,
315 });
316 }
317 if let Some(t) = m.latent_kv.find_in(ts) {
318 return Some(BlockKv::Latent {
319 bytes_per_token: out_of(t)? as u64 * elem,
320 });
321 }
322 if let Some(t) = m.attn_k.find_in(ts) {
323 return Some(BlockKv::Attention {
324 bytes_per_token: 2 * out_of(t)? as u64 * elem,
325 });
326 }
327 None
328}
329
330fn infer_kv(entries: &[TensorEntry], opts: &CostOptions) -> KvProfile {
336 use std::collections::BTreeMap;
337 let elem = kv_elem_bytes(&opts.kv_dtype);
338 let mut blocks: BTreeMap<usize, Vec<&TensorEntry>> = BTreeMap::new();
339 for t in entries {
340 if let Some(i) = opts.block_of(&t.name) {
341 blocks.entry(i).or_default().push(t);
342 }
343 }
344 if blocks.is_empty() {
345 return KvProfile::unknown();
346 }
347 match blocks
348 .values()
349 .map(|ts| classify_block(ts, &opts.kv, elem))
350 .collect::<Option<Vec<_>>>()
351 {
352 Some(per_block) => KvProfile::inferred(per_block),
353 None => KvProfile::unknown(),
354 }
355}
356
357#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
363pub enum KvSource {
364 Inferred,
366 Declared,
368 #[default]
370 Unknown,
371}
372
373#[derive(Debug, Clone, Default)]
375pub struct KvProfile {
376 detail: KvDetail,
377 source: KvSource,
378}
379
380impl KvProfile {
381 pub fn declared(bytes_per_layer_token: u64) -> Self {
382 Self {
383 detail: KvDetail::Flat {
384 bytes_per_layer_token,
385 },
386 source: KvSource::Declared,
387 }
388 }
389 pub fn inferred(blocks: Vec<BlockKv>) -> Self {
390 Self {
391 detail: KvDetail::PerBlock(blocks),
392 source: KvSource::Inferred,
393 }
394 }
395 pub fn unknown() -> Self {
396 Self::default()
397 }
398
399 pub fn source(&self) -> KvSource {
400 self.source
401 }
402 pub fn is_known(&self) -> bool {
403 self.source != KvSource::Unknown
404 }
405 pub fn blocks(&self) -> &[BlockKv] {
407 match &self.detail {
408 KvDetail::PerBlock(b) => b,
409 KvDetail::Flat { .. } => &[],
410 }
411 }
412
413 pub fn bytes_per_layer_token(&self) -> u64 {
418 match &self.detail {
419 KvDetail::Flat {
420 bytes_per_layer_token,
421 } => *bytes_per_layer_token,
422 KvDetail::PerBlock(b) if !b.is_empty() => {
423 b.iter().map(BlockKv::bytes_per_token).sum::<u64>() / b.len() as u64
424 }
425 KvDetail::PerBlock(_) => 0,
426 }
427 }
428 pub fn state_bytes_per_layer(&self) -> u64 {
430 match &self.detail {
431 KvDetail::Flat { .. } => 0,
432 KvDetail::PerBlock(b) if !b.is_empty() => {
433 b.iter().map(BlockKv::state_bytes).sum::<u64>() / b.len() as u64
434 }
435 KvDetail::PerBlock(_) => 0,
436 }
437 }
438
439 pub fn bytes(&self, layers: u64, context: usize) -> u64 {
441 self.bytes_per_layer_token()
442 .saturating_mul(layers)
443 .saturating_mul(context as u64)
444 .saturating_add(self.state_bytes_per_layer().saturating_mul(layers))
445 }
446
447 pub fn bytes_for(&self, layers: std::ops::Range<usize>, context: usize) -> u64 {
451 let b = self.blocks();
452 if b.is_empty() {
453 return self.bytes(layers.len() as u64, context);
454 }
455 let hi = layers.end.min(b.len());
456 let lo = layers.start.min(hi);
457 window_bytes(&b[lo..hi], context)
458 }
459
460 pub fn max_window_bytes(&self, len: usize, context: usize) -> u64 {
467 let b = self.blocks();
468 if b.is_empty() || len == 0 {
469 return self.bytes(len as u64, context);
470 }
471 b.windows(len.min(b.len()))
472 .map(|w| window_bytes(w, context))
473 .max()
474 .unwrap_or(0)
475 }
476
477 pub fn summary(&self, layers: u64, context: usize) -> String {
478 let Some(kind) = (match self.source {
479 KvSource::Unknown => None,
480 KvSource::Inferred => Some("inferred"),
481 KvSource::Declared => Some("declared"),
482 }) else {
483 return "KV: unknown".into();
484 };
485 let state = self.state_bytes_per_layer() * layers;
486 format!(
487 "KV: {:.2} GB for {layers} layers @ {context} tokens ({kind}{})",
488 self.bytes(layers, context) as f64 / 1e9,
489 if state > 0 {
490 format!(", + {:.2} GB recurrent state", state as f64 / 1e9)
491 } else {
492 String::new()
493 },
494 )
495 }
496}
497
498fn window_bytes(blocks: &[BlockKv], context: usize) -> u64 {
500 let per_token: u64 = blocks.iter().map(BlockKv::bytes_per_token).sum();
501 let state: u64 = blocks.iter().map(BlockKv::state_bytes).sum();
502 per_token
503 .saturating_mul(context as u64)
504 .saturating_add(state)
505}
506
507#[derive(Debug, Clone, Default)]
513pub struct ModelCost {
514 pub n_layers: usize,
515 pub per_layer_bytes: u64,
517 pub per_layer_expert_bytes: u64,
519 pub per_layer_expert_active_bytes: u64,
522 pub embed_bytes: u64,
524 pub head_bytes: u64,
526 pub per_layer_flops: f64,
528 pub kv: KvProfile,
534 pub params: u64,
536 pub hidden_size: usize,
543}
544
545struct Width {
548 bytes: u64,
549 bits_per_param: f64,
550}
551
552impl Width {
553 fn of(bytes: u64, total_bytes: u64, total_elems: u64) -> Self {
555 let bits_per_param = if total_elems > 0 {
556 total_bytes as f64 * 8.0 / total_elems as f64
557 } else {
558 0.0
559 };
560 Self {
561 bytes,
562 bits_per_param,
563 }
564 }
565 fn params(&self) -> f64 {
566 if self.bits_per_param > 0.0 {
567 self.bytes as f64 * 8.0 / self.bits_per_param
568 } else {
569 0.0
570 }
571 }
572}
573
574fn decode_flops_per_layer(dense: Width, active_experts: Width) -> f64 {
592 2.0 * (dense.params() + active_experts.params())
593}
594
595impl ModelCost {
596 pub fn at_bits(&self, target_bits: f64) -> Option<Self> {
606 if self.params == 0 || target_bits <= 0.0 {
607 return None;
608 }
609 let cur = self.total_bytes() as f64 * 8.0 / self.params as f64;
610 if cur <= 0.0 {
611 return None;
612 }
613 let r = target_bits / cur;
614 let sc = |b: u64| (b as f64 * r) as u64;
615 Some(Self {
616 per_layer_bytes: sc(self.per_layer_bytes),
617 per_layer_expert_bytes: sc(self.per_layer_expert_bytes),
618 per_layer_expert_active_bytes: sc(self.per_layer_expert_active_bytes),
619 embed_bytes: sc(self.embed_bytes),
620 head_bytes: sc(self.head_bytes),
621 ..self.clone()
623 })
624 }
625
626 pub fn kv_bytes(&self, layers: u64, context: usize) -> u64 {
628 self.kv.bytes(layers, context)
629 }
630
631 pub fn total_bytes(&self) -> u64 {
633 self.embed_bytes
634 + self.head_bytes
635 + (self.per_layer_bytes + self.per_layer_expert_bytes) * self.n_layers as u64
636 }
637
638 pub fn total_resident_bytes(&self) -> u64 {
640 self.embed_bytes + self.head_bytes + self.per_layer_bytes * self.n_layers as u64
641 }
642
643 pub fn is_expert_dominated(&self) -> bool {
646 self.per_layer_expert_bytes > self.per_layer_bytes
647 }
648
649 pub fn from_tensor_index(entries: &[TensorEntry], opts: &CostOptions) -> Result<Self> {
655 if entries.is_empty() {
656 bail!("empty tensor index");
657 }
658 let mut max_block: Option<usize> = None;
659 let (mut layer_bytes, mut expert_bytes) = (0u64, 0u64);
660 let (mut layer_elems, mut expert_elems) = (0u64, 0u64);
665 let (mut embed, mut head) = (0u64, 0u64);
666
667 for t in entries {
668 match opts.block_of(&t.name) {
669 Some(i) => {
670 max_block = Some(max_block.map_or(i, |m: usize| m.max(i)));
671 if opts.is_expert(&t.name) {
672 expert_bytes += t.bytes;
673 expert_elems += t.elements();
674 } else {
675 layer_bytes += t.bytes;
676 layer_elems += t.elements();
677 }
678 }
679 None => {
680 if opts.embed_markers.matches(&t.name) {
683 embed += t.bytes;
684 } else if opts.head_markers.matches(&t.name) {
685 head += t.bytes;
686 }
687 }
688 }
689 }
690
691 let n_layers = max_block.map(|m| m + 1).unwrap_or(0);
692 if n_layers == 0 {
693 bail!(
694 "no numbered blocks found — checked prefixes {:?}; pass CostOptions \
695 with the checkpoint's naming convention",
696 opts.block_prefixes
697 );
698 }
699 let n = n_layers as u64;
700 let per_layer_expert_bytes = expert_bytes / n;
701 let per_layer_bytes = layer_bytes / n;
702 let per_layer_expert_active_bytes =
703 (per_layer_expert_bytes as f64 * opts.expert_active_fraction.clamp(0.0, 1.0)) as u64;
704 let params: u64 = entries.iter().map(|t| t.elements()).sum();
705 Ok(Self {
706 n_layers,
707 per_layer_bytes,
708 per_layer_expert_bytes,
709 per_layer_expert_active_bytes,
710 embed_bytes: embed,
711 head_bytes: head,
712 per_layer_flops: decode_flops_per_layer(
713 Width::of(per_layer_bytes, layer_bytes, layer_elems),
714 Width::of(per_layer_expert_active_bytes, expert_bytes, expert_elems),
715 ) * opts.per_layer_flops,
716 hidden_size: infer_hidden(entries, opts),
717 kv: infer_kv(entries, opts),
718 params,
719 })
720 }
721
722 pub fn summary(&self) -> String {
724 let gb = |b: u64| b as f64 / 1e9;
725 if self.per_layer_expert_bytes > 0 {
726 format!(
727 "{} layers | {:.2} GB resident + {:.2} GB experts = {:.2} GB \
728 | per layer {:.2} GB + {:.2} GB experts ({:.2} GB active)",
729 self.n_layers,
730 gb(self.total_resident_bytes()),
731 gb(self.per_layer_expert_bytes * self.n_layers as u64),
732 gb(self.total_bytes()),
733 gb(self.per_layer_bytes),
734 gb(self.per_layer_expert_bytes),
735 gb(self.per_layer_expert_active_bytes),
736 )
737 } else {
738 format!(
739 "{} layers | {:.2} GB | per layer {:.2} GB",
740 self.n_layers,
741 gb(self.total_bytes()),
742 gb(self.per_layer_bytes),
743 )
744 }
745 }
746}
747
748#[cfg(feature = "gguf")]
749mod from_gguf {
750 use super::*;
751 use std::path::Path;
752
753 pub fn tensor_index(path: impl AsRef<Path>) -> Result<Vec<TensorEntry>> {
759 tensor_index_multi(std::slice::from_ref(&path.as_ref().to_path_buf()))
760 }
761
762 pub fn tensor_index_multi(paths: &[std::path::PathBuf]) -> Result<Vec<TensorEntry>> {
767 let mut out = Vec::new();
768 for p in paths {
769 let f = rlx_gguf::GgufFile::from_path(p)?;
770 for t in f.tensors.values() {
771 let bytes =
772 rlx_gguf::bytes_for_public(t.dtype, t.n_elements()).ok_or_else(|| {
773 anyhow::anyhow!("{}: unsupported ggml type {:?}", t.name, t.dtype)
774 })? as u64;
775 out.push(TensorEntry::new(t.name.clone(), t.shape.clone(), bytes));
776 }
777 }
778 Ok(out)
779 }
780
781 impl ModelCost {
782 pub fn from_gguf(paths: &[std::path::PathBuf], opts: &CostOptions) -> Result<Self> {
784 Self::from_tensor_index(&tensor_index_multi(paths)?, opts)
785 }
786 }
787}
788
789#[cfg(feature = "gguf")]
790pub use from_gguf::{tensor_index, tensor_index_multi};
791
792#[cfg(test)]
793mod tests {
794 use super::*;
795
796 #[test]
805 fn per_layer_flops_is_a_real_decode_flop_count() {
806 let idx = moe_index();
807 let opts = CostOptions {
809 expert_active_fraction: 8.0 / 288.0,
810 ..Default::default()
811 };
812 let c = ModelCost::from_tensor_index(&idx, &opts).unwrap();
813
814 let dense_elems: u64 = idx
818 .iter()
819 .filter(|t| opts.block_of(&t.name).is_some() && !opts.is_expert(&t.name))
820 .map(|t| t.elements())
821 .sum();
822 let expert_elems: u64 = idx
823 .iter()
824 .filter(|t| opts.block_of(&t.name).is_some() && opts.is_expert(&t.name))
825 .map(|t| t.elements())
826 .sum();
827 let active = dense_elems as f64 + expert_elems as f64 * opts.expert_active_fraction;
828 let want = 2.0 * active;
829 let got = c.per_layer_flops * c.n_layers as f64;
830 assert!(
831 (got - want).abs() <= want * 0.02,
832 "stack FLOPs/token {got:.3e} against 2 x active params {want:.3e}"
833 );
834 assert!(
835 c.per_layer_flops > 1.0,
836 "still the dimensionless placeholder: {}",
837 c.per_layer_flops
838 );
839 }
840
841 #[test]
845 fn mixed_precision_groups_use_their_own_bit_width() {
846 let mut idx = moe_index();
848 for t in &mut idx {
849 if t.name.contains("_exps") {
850 t.bytes /= 4;
851 }
852 }
853 let opts = CostOptions {
854 expert_active_fraction: 8.0 / 288.0,
855 ..Default::default()
856 };
857 let c = ModelCost::from_tensor_index(&idx, &opts).unwrap();
858
859 let dense_elems: u64 = idx
860 .iter()
861 .filter(|t| opts.block_of(&t.name).is_some() && !opts.is_expert(&t.name))
862 .map(|t| t.elements())
863 .sum();
864 let expert_elems: u64 = idx
865 .iter()
866 .filter(|t| opts.block_of(&t.name).is_some() && opts.is_expert(&t.name))
867 .map(|t| t.elements())
868 .sum();
869 let want = 2.0 * (dense_elems as f64 + expert_elems as f64 * opts.expert_active_fraction);
872 let got = c.per_layer_flops * c.n_layers as f64;
873 assert!(
874 (got - want).abs() <= want * 0.02,
875 "re-quantizing the experts moved the FLOP count to {got:.3e} from \
876 {want:.3e}; a single average width was used for both groups"
877 );
878 }
879
880 fn moe_index() -> Vec<TensorEntry> {
883 let mut v = vec![
884 TensorEntry::new("token_embd.weight", vec![4096, 154880], 357_000_000),
885 TensorEntry::new("output.weight", vec![4096, 154880], 357_000_000),
886 TensorEntry::new("output_norm.weight", vec![4096], 16_384),
887 ];
888 for i in 0..4 {
889 v.push(TensorEntry::new(
890 format!("blk.{i}.attn_q.weight"),
891 vec![4096, 8192],
892 23_000_000,
893 ));
894 v.push(TensorEntry::new(
895 format!("blk.{i}.attn_norm.weight"),
896 vec![4096],
897 16_384,
898 ));
899 if i >= 1 {
900 for b in ["ffn_gate_exps", "ffn_up_exps", "ffn_down_exps"] {
901 v.push(TensorEntry::new(
902 format!("blk.{i}.{b}.weight"),
903 vec![288, 2048, 4096],
904 600_000_000,
905 ));
906 }
907 }
908 }
909 v
910 }
911
912 #[test]
913 fn derives_layers_and_splits_experts_from_dense() {
914 let opts = CostOptions::default().with_experts(8, 288);
915 let c = ModelCost::from_tensor_index(&moe_index(), &opts).unwrap();
916 assert_eq!(c.n_layers, 4);
917 assert_eq!(c.embed_bytes, 357_000_000);
918 assert_eq!(c.head_bytes, 357_000_000 + 16_384);
920 assert_eq!(c.per_layer_bytes, 23_016_384);
922 assert_eq!(c.per_layer_expert_bytes, 3 * 3 * 600_000_000 / 4);
924 assert!(c.is_expert_dominated());
925 }
926
927 #[test]
930 fn active_expert_bytes_track_the_router() {
931 let all = CostOptions::default();
932 let sparse = CostOptions::default().with_experts(8, 288);
933 let idx = moe_index();
934 let a = ModelCost::from_tensor_index(&idx, &all).unwrap();
935 let s = ModelCost::from_tensor_index(&idx, &sparse).unwrap();
936 assert_eq!(a.per_layer_expert_active_bytes, a.per_layer_expert_bytes);
937 assert!(s.per_layer_expert_active_bytes * 30 < s.per_layer_expert_bytes);
938 assert_eq!(s.per_layer_expert_bytes, a.per_layer_expert_bytes);
939 }
940
941 #[test]
944 fn rank_three_non_expert_tensors_stay_resident() {
945 let idx = vec![
946 TensorEntry::new("blk.0.attn_k_b.weight", vec![64, 512, 256], 9_000_000),
947 TensorEntry::new("blk.0.attn_norm.weight", vec![4096], 16_384),
948 ];
949 let c = ModelCost::from_tensor_index(&idx, &CostOptions::default()).unwrap();
950 assert_eq!(c.per_layer_expert_bytes, 0);
951 assert_eq!(c.per_layer_bytes, 9_016_384);
952 assert!(!c.is_expert_dominated());
953 }
954
955 #[test]
956 fn hf_naming_works_too() {
957 let idx = vec![
958 TensorEntry::new("model.embed_tokens.weight", vec![32, 8], 1024),
959 TensorEntry::new("model.layers.0.self_attn.q_proj.weight", vec![8, 8], 256),
960 TensorEntry::new("model.layers.1.self_attn.q_proj.weight", vec![8, 8], 256),
961 TensorEntry::new("model.layers.1.mlp.experts.0.up.weight", vec![8, 8], 4096),
962 TensorEntry::new("lm_head.weight", vec![32, 8], 1024),
963 ];
964 let c = ModelCost::from_tensor_index(&idx, &CostOptions::default()).unwrap();
965 assert_eq!(c.n_layers, 2);
966 assert_eq!(c.embed_bytes, 1024);
967 assert_eq!(c.head_bytes, 1024);
968 assert_eq!(c.per_layer_expert_bytes, 2048);
969 }
970
971 #[test]
974 fn at_bits_scales_weights_only() {
975 let idx = moe_index();
976 let mut c = ModelCost::from_tensor_index(&idx, &CostOptions::default()).unwrap();
977 c.kv = KvProfile::declared(4096);
978 let before = c.total_bytes();
979 let half = c
980 .at_bits(c.total_bytes() as f64 * 8.0 / c.params as f64 / 2.0)
981 .unwrap();
982 assert!(
983 (half.total_bytes() as f64 - before as f64 / 2.0).abs() < before as f64 * 0.01,
984 "halving the width should roughly halve the bytes"
985 );
986 assert_eq!(half.n_layers, c.n_layers);
987 assert_eq!(
988 half.kv.bytes_per_layer_token(),
989 c.kv.bytes_per_layer_token()
990 );
991 }
992
993 #[test]
994 fn at_bits_is_none_without_a_parameter_count() {
995 let c = ModelCost {
996 params: 0,
997 ..Default::default()
998 };
999 assert!(c.at_bits(4.0).is_none());
1000 }
1001
1002 #[test]
1003 fn kv_bytes_scale_with_layers_and_context() {
1004 let c = ModelCost {
1005 kv: KvProfile::declared(1024),
1006 ..Default::default()
1007 };
1008 assert_eq!(c.kv_bytes(4, 100), 4 * 100 * 1024);
1009 assert_eq!(c.kv_bytes(0, 100), 0);
1010 }
1011
1012 fn hybrid_index() -> Vec<TensorEntry> {
1017 let (hidden, kv_lora, heads, head_dim) = (4096usize, 512usize, 64usize, 128usize);
1018 let mut v = Vec::new();
1019 for i in 0..45 {
1020 v.push(TensorEntry::new(
1021 format!("blk.{i}.attn_norm.weight"),
1022 vec![hidden],
1023 hidden as u64 * 4,
1024 ));
1025 if i % 4 == 3 {
1026 v.push(TensorEntry::new(
1028 format!("blk.{i}.attn_kv_a_mqa.weight"),
1029 vec![hidden, kv_lora],
1030 1,
1031 ));
1032 } else {
1033 v.push(TensorEntry::new(format!("blk.{i}.ssm_a"), vec![heads], 1));
1034 v.push(TensorEntry::new(
1035 format!("blk.{i}.ssm_norm.weight"),
1036 vec![head_dim],
1037 1,
1038 ));
1039 }
1040 }
1041 v
1042 }
1043
1044 #[test]
1048 fn infers_hybrid_recurrent_plus_latent_attention() {
1049 let c = ModelCost::from_tensor_index(&hybrid_index(), &CostOptions::default()).unwrap();
1050 assert_eq!(c.kv.source(), KvSource::Inferred);
1051 assert_eq!(c.kv.bytes_per_layer_token(), 11 * 512 * 4 / 45);
1053 assert_eq!(c.kv.state_bytes_per_layer(), 34 * 64 * 128 * 128 * 4 / 45);
1055
1056 let total = c.kv.bytes(45, 131_072);
1058 assert!(
1059 (total as f64 / 1e9 - 3.09).abs() < 0.05,
1060 "expected ~3.09 GB, got {:.2} GB",
1061 total as f64 / 1e9
1062 );
1063 }
1064
1065 #[test]
1067 fn infers_gqa_attention_as_two_times_k_width() {
1068 let (hidden, kv_width) = (4096usize, 1024usize);
1069 let idx = vec![
1070 TensorEntry::new("blk.0.attn_norm.weight", vec![hidden], 1),
1071 TensorEntry::new("blk.0.attn_k.weight", vec![kv_width, hidden], 1),
1072 ];
1073 let c = ModelCost::from_tensor_index(&idx, &CostOptions::default()).unwrap();
1074 assert_eq!(c.kv.source(), KvSource::Inferred);
1075 assert_eq!(c.kv.bytes_per_layer_token(), 2 * kv_width as u64 * 4);
1076 }
1077
1078 #[test]
1081 fn kv_inference_is_orientation_agnostic() {
1082 let (hidden, w) = (4096usize, 1024usize);
1083 let gguf = vec![
1084 TensorEntry::new("blk.0.attn_norm.weight", vec![hidden], 1),
1085 TensorEntry::new("blk.0.attn_k.weight", vec![hidden, w], 1),
1086 ];
1087 let hf = vec![
1088 TensorEntry::new("blk.0.attn_norm.weight", vec![hidden], 1),
1089 TensorEntry::new("blk.0.attn_k.weight", vec![w, hidden], 1),
1090 ];
1091 let a = ModelCost::from_tensor_index(&gguf, &CostOptions::default()).unwrap();
1092 let b = ModelCost::from_tensor_index(&hf, &CostOptions::default()).unwrap();
1093 assert_eq!(a.kv.bytes_per_layer_token(), b.kv.bytes_per_layer_token());
1094 assert_eq!(a.kv.bytes_per_layer_token(), 2 * w as u64 * 4);
1095 }
1096
1097 #[test]
1100 fn unreadable_attention_is_unknown_not_zero() {
1101 let idx = vec![
1102 TensorEntry::new("blk.0.attn_norm.weight", vec![4096], 1),
1103 TensorEntry::new("blk.0.mystery_attention.weight", vec![7, 9], 1),
1104 ];
1105 let c = ModelCost::from_tensor_index(&idx, &CostOptions::default()).unwrap();
1106 assert_eq!(c.kv.source(), KvSource::Unknown);
1107 assert!(!c.kv.is_known());
1108 assert_eq!(c.kv.bytes_per_layer_token(), 0);
1109 }
1110
1111 #[test]
1119 fn recurrent_block_with_unreadable_dims_is_unknown() {
1120 let idx = vec![
1122 TensorEntry::new("blk.0.attn_norm.weight", vec![4096], 1),
1123 TensorEntry::new("blk.0.ssm_conv1d.weight", vec![8192, 1, 4], 1),
1124 ];
1125 let c = ModelCost::from_tensor_index(&idx, &CostOptions::default()).unwrap();
1126 assert_eq!(c.kv.source(), KvSource::Unknown);
1127 assert!(!c.kv.is_known());
1128 }
1129
1130 #[test]
1134 fn recurrent_wins_over_attn_k_in_the_same_block() {
1135 let idx = vec![
1136 TensorEntry::new("blk.0.attn_norm.weight", vec![4096], 1),
1137 TensorEntry::new("blk.0.attn_k.weight", vec![8192, 4096], 1),
1138 TensorEntry::new("blk.0.ssm_a", vec![64], 1),
1139 TensorEntry::new("blk.0.ssm_norm.weight", vec![128], 1),
1140 ];
1141 let c = ModelCost::from_tensor_index(&idx, &CostOptions::default()).unwrap();
1142 assert_eq!(
1143 c.kv.blocks(),
1144 [BlockKv::Recurrent {
1145 state_bytes: 64 * 128 * 128 * 4
1146 }]
1147 );
1148 assert_eq!(c.kv.bytes_per_layer_token(), 0, "no per-token cache");
1149 }
1150
1151 #[test]
1154 fn partially_readable_stack_is_unknown() {
1155 let mut idx = hybrid_index();
1156 idx.retain(|t| !t.name.starts_with("blk.7."));
1157 idx.push(TensorEntry::new("blk.7.attn_norm.weight", vec![4096], 1));
1158 idx.push(TensorEntry::new("blk.7.something.weight", vec![3, 5], 1));
1159 let c = ModelCost::from_tensor_index(&idx, &CostOptions::default()).unwrap();
1160 assert_eq!(c.kv.source(), KvSource::Unknown);
1161 }
1162
1163 #[test]
1166 fn square_projection_is_ambiguous_and_declines() {
1167 let idx = vec![
1168 TensorEntry::new("blk.0.attn_norm.weight", vec![4096], 1),
1169 TensorEntry::new("blk.0.attn_k.weight", vec![4096, 4096], 1),
1170 ];
1171 let c = ModelCost::from_tensor_index(&idx, &CostOptions::default()).unwrap();
1172 assert_eq!(c.kv.source(), KvSource::Unknown);
1173 }
1174
1175 #[test]
1179 fn per_range_kv_beats_the_average_on_a_hybrid_stack() {
1180 let c = ModelCost::from_tensor_index(&hybrid_index(), &CostOptions::default()).unwrap();
1181 let ctx = 4096;
1182 let none = c.kv.bytes_for(0..3, ctx); let one = c.kv.bytes_for(3..7, ctx); let avg3 = c.kv.bytes(3, ctx);
1185
1186 assert_eq!(
1188 none,
1189 c.kv.blocks()[0..3]
1190 .iter()
1191 .map(BlockKv::state_bytes)
1192 .sum::<u64>()
1193 );
1194 assert_eq!(
1196 one,
1197 512 * 4 * ctx as u64
1198 + c.kv.blocks()[3..7]
1199 .iter()
1200 .map(BlockKv::state_bytes)
1201 .sum::<u64>()
1202 );
1203 assert!(
1205 avg3 > none,
1206 "average over-reserves the recurrent-only range"
1207 );
1208 assert!(
1209 c.kv.bytes(4, ctx) < one,
1210 "average under-reserves the range holding an attention layer"
1211 );
1212 }
1213
1214 #[test]
1218 fn window_max_bounds_every_placement_of_a_given_length() {
1219 let c = ModelCost::from_tensor_index(&hybrid_index(), &CostOptions::default()).unwrap();
1220 let ctx = 4096;
1221 for len in [1usize, 3, 4, 8, 45] {
1222 let worst = c.kv.max_window_bytes(len, ctx);
1223 for start in 0..=(45 - len) {
1224 assert!(
1225 c.kv.bytes_for(start..start + len, ctx) <= worst,
1226 "window {start}..{} exceeds the bound at len {len}",
1227 start + len
1228 );
1229 }
1230 assert!(
1231 worst >= c.kv.bytes(len as u64, ctx),
1232 "bound must cover the mean"
1233 );
1234 }
1235 }
1236
1237 #[test]
1240 fn cache_dtype_scales_the_reservation() {
1241 let f32o = CostOptions::default();
1242 let f16o = CostOptions {
1243 kv_dtype: "f16".into(),
1244 ..Default::default()
1245 };
1246 let a = ModelCost::from_tensor_index(&hybrid_index(), &f32o).unwrap();
1247 let b = ModelCost::from_tensor_index(&hybrid_index(), &f16o).unwrap();
1248 assert_eq!(
1251 a.kv.blocks()[3].bytes_per_token(),
1252 2 * b.kv.blocks()[3].bytes_per_token()
1253 );
1254 assert_eq!(
1256 a.kv.blocks()[0].state_bytes(),
1257 2 * b.kv.blocks()[0].state_bytes()
1258 );
1259 assert!(a.kv.blocks()[0].state_bytes() > 0);
1260 assert_eq!(kv_elem_bytes("bf16"), 2);
1261 assert_eq!(kv_elem_bytes("int8"), 1);
1262 assert_eq!(kv_elem_bytes("anything-else"), 4);
1263 }
1264
1265 #[test]
1268 fn hidden_size_comes_from_the_checkpoint() {
1269 let c = ModelCost::from_tensor_index(&hybrid_index(), &CostOptions::default()).unwrap();
1270 assert_eq!(c.hidden_size, 4096);
1271 let guessed = ((c.per_layer_bytes as f64 / 12.0).sqrt()).max(1.0) as usize;
1273 assert_ne!(guessed, c.hidden_size);
1274 }
1275
1276 #[test]
1279 fn declared_profile_falls_back_to_the_average_for_ranges() {
1280 let k = KvProfile::declared(1000);
1281 assert_eq!(k.bytes_for(0..4, 10), 4 * 10 * 1000);
1282 assert_eq!(k.max_window_bytes(4, 10), 4 * 10 * 1000);
1283 }
1284
1285 #[test]
1286 fn unrecognized_naming_is_an_error_not_a_zero_layer_model() {
1287 let idx = vec![TensorEntry::new("weird.0.w", vec![4], 16)];
1288 let err = ModelCost::from_tensor_index(&idx, &CostOptions::default())
1289 .unwrap_err()
1290 .to_string();
1291 assert!(err.contains("no numbered blocks"), "{err}");
1292 }
1293}