1use std::collections::BTreeMap;
26use std::path::Path;
27
28use serde::{Deserialize, Serialize};
29
30use super::rdu_chip::{Fact, Provenance};
31use super::roofs::{Engine, MemLevel, Rate, Roofs, StreamMix};
32use crate::codegen_sva::fabric_target::Precision;
33
34pub const GGML_TYPES_SOURCE: &str = "github.com/ggml-org/llama.cpp: ggml/include/ggml.h `enum ggml_type`; \
38 ggml/src/ggml.c `type_traits` (.blck_size, .type_size = sizeof(block_*)); ggml/src/ggml-common.h \
39 `static_assert(sizeof(block_*) == …)`";
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
43#[allow(non_camel_case_types)]
44pub enum GgmlType {
45 F32,
46 F16,
47 Q4_0,
48 Q4_1,
49 Q5_0,
50 Q5_1,
51 Q8_0,
52 Q8_1,
53 Q2K,
54 Q3K,
55 Q4K,
56 Q5K,
57 Q6K,
58 Q8K,
59 Iq2Xxs,
60 Iq2Xs,
61 Iq3Xxs,
62 Iq1S,
63 Iq4Nl,
64 Iq3S,
65 Iq2S,
66 Iq4Xs,
67 I8,
68 I16,
69 I32,
70 I64,
71 F64,
72 Iq1M,
73 Bf16,
74 Tq1_0,
75 Tq2_0,
76 Mxfp4,
77 Nvfp4,
78 Q1_0,
79 Q2_0,
80}
81
82impl GgmlType {
83 pub const ALL: [GgmlType; 35] = [
85 GgmlType::F32,
86 GgmlType::F16,
87 GgmlType::Q4_0,
88 GgmlType::Q4_1,
89 GgmlType::Q5_0,
90 GgmlType::Q5_1,
91 GgmlType::Q8_0,
92 GgmlType::Q8_1,
93 GgmlType::Q2K,
94 GgmlType::Q3K,
95 GgmlType::Q4K,
96 GgmlType::Q5K,
97 GgmlType::Q6K,
98 GgmlType::Q8K,
99 GgmlType::Iq2Xxs,
100 GgmlType::Iq2Xs,
101 GgmlType::Iq3Xxs,
102 GgmlType::Iq1S,
103 GgmlType::Iq4Nl,
104 GgmlType::Iq3S,
105 GgmlType::Iq2S,
106 GgmlType::Iq4Xs,
107 GgmlType::I8,
108 GgmlType::I16,
109 GgmlType::I32,
110 GgmlType::I64,
111 GgmlType::F64,
112 GgmlType::Iq1M,
113 GgmlType::Bf16,
114 GgmlType::Tq1_0,
115 GgmlType::Tq2_0,
116 GgmlType::Mxfp4,
117 GgmlType::Nvfp4,
118 GgmlType::Q1_0,
119 GgmlType::Q2_0,
120 ];
121
122 fn spec(self) -> (u32, u64, u64) {
124 use GgmlType::*;
125 match self {
126 F32 => (0, 1, 4),
127 F16 => (1, 1, 2),
128 Q4_0 => (2, 32, 18),
129 Q4_1 => (3, 32, 20),
130 Q5_0 => (6, 32, 22),
131 Q5_1 => (7, 32, 24),
132 Q8_0 => (8, 32, 34),
133 Q8_1 => (9, 32, 36),
134 Q2K => (10, 256, 84),
135 Q3K => (11, 256, 110),
136 Q4K => (12, 256, 144),
137 Q5K => (13, 256, 176),
138 Q6K => (14, 256, 210),
139 Q8K => (15, 256, 292),
140 Iq2Xxs => (16, 256, 66),
141 Iq2Xs => (17, 256, 74),
142 Iq3Xxs => (18, 256, 98),
143 Iq1S => (19, 256, 50),
144 Iq4Nl => (20, 32, 18),
145 Iq3S => (21, 256, 110),
146 Iq2S => (22, 256, 82),
147 Iq4Xs => (23, 256, 136),
148 I8 => (24, 1, 1),
149 I16 => (25, 1, 2),
150 I32 => (26, 1, 4),
151 I64 => (27, 1, 8),
152 F64 => (28, 1, 8),
153 Iq1M => (29, 256, 56),
154 Bf16 => (30, 1, 2),
155 Tq1_0 => (34, 256, 54),
156 Tq2_0 => (35, 256, 66),
157 Mxfp4 => (39, 32, 17),
158 Nvfp4 => (40, 64, 36),
159 Q1_0 => (41, 128, 18),
160 Q2_0 => (42, 64, 18),
161 }
162 }
163
164 pub fn id(self) -> u32 {
165 self.spec().0
166 }
167
168 pub fn block_elems(self) -> u64 {
169 self.spec().1
170 }
171
172 pub fn block_bytes(self) -> u64 {
173 self.spec().2
174 }
175
176 pub fn from_id(id: u32) -> Option<GgmlType> {
178 GgmlType::ALL.into_iter().find(|t| t.id() == id)
179 }
180
181 pub fn bytes(self, elems: u64) -> Option<u64> {
183 (elems % self.block_elems() == 0).then(|| elems / self.block_elems() * self.block_bytes())
184 }
185}
186
187#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
191pub struct TensorRow {
192 pub name: String,
193 pub dims: Vec<u64>,
195 #[serde(rename = "type")]
196 pub type_id: u32,
197 pub offset: u64,
199}
200
201#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
203pub struct GgufHeader {
204 pub metadata: BTreeMap<String, serde_json::Value>,
205 pub file_bytes: u64,
206 pub data_offset: u64,
207 pub tensors: Vec<TensorRow>,
208}
209
210#[derive(Deserialize)]
211struct HeaderLedger {
212 header: GgufHeader,
213}
214
215pub struct HeaderSource {
220 arch: String,
221 meta: BTreeMap<String, crate::model::gguf::MetaValue>,
222 dims: BTreeMap<String, Vec<u64>>,
223 names: Vec<String>,
224}
225
226impl crate::model::spec::SpecSource for HeaderSource {
227 fn arch(&self) -> &str {
228 &self.arch
229 }
230 fn arch_meta(&self, suffix: &str) -> Option<&crate::model::gguf::MetaValue> {
231 self.meta.get(&format!("{}.{suffix}", self.arch))
232 }
233 fn dims(&self, name: &str) -> Option<&[u64]> {
234 self.dims.get(name).map(|d| d.as_slice())
235 }
236 fn tensor_names(&self) -> Vec<&str> {
237 self.names.iter().map(|n| n.as_str()).collect()
238 }
239}
240
241impl crate::model::reference::WeightSource for GgufHeader {
244 fn describe(&self, name: &str) -> std::io::Result<(crate::model::quant::GgmlType, Vec<u64>)> {
245 let t = self.tensors.iter().find(|t| t.name == name).ok_or_else(|| std::io::Error::other(format!("the header holds no {name}")))?;
246 let ty = crate::model::quant::GgmlType::from_id(t.type_id).map_err(|id| std::io::Error::other(format!("{name}: ggml type {id}")))?;
247 Ok((ty, t.dims.clone()))
248 }
249 fn row_bytes(&mut self, name: &str, _first: usize, _count: usize) -> std::io::Result<Vec<u8>> {
250 Err(std::io::Error::other(format!("a header holds no bytes of {name}")))
251 }
252 fn has(&self, name: &str) -> bool {
253 self.tensors.iter().any(|t| t.name == name)
254 }
255}
256
257impl GgufHeader {
258 pub fn read_ledger(path: &Path) -> Result<GgufHeader, String> {
261 let text = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
262 let ledger: HeaderLedger = serde_json::from_str(&text).map_err(|e| e.to_string())?;
263 Ok(ledger.header)
264 }
265
266 pub fn source(&self) -> HeaderSource {
268 use crate::model::gguf::MetaValue;
269 fn value(v: &serde_json::Value) -> Option<MetaValue> {
270 Some(match v {
271 serde_json::Value::Bool(b) => MetaValue::Bool(*b),
272 serde_json::Value::Number(n) => match (n.as_u64(), n.as_i64()) {
273 (Some(u), _) => MetaValue::U64(u),
274 (None, Some(i)) => MetaValue::I64(i),
275 _ => {
276 let v = n.as_f64()?;
277 if (v as f32) as f64 == v {
278 MetaValue::F32(v as f32)
279 } else {
280 MetaValue::F64(v)
281 }
282 }
283 },
284 serde_json::Value::String(s) => MetaValue::Str(s.as_bytes().to_vec()),
285 serde_json::Value::Array(a) => {
286 let items: Vec<MetaValue> = a.iter().map(value).collect::<Option<_>>()?;
287 MetaValue::Array { ty: items.first().map_or(8, MetaValue::gguf_type), items }
288 }
289 serde_json::Value::Null | serde_json::Value::Object(_) => return None,
290 })
291 }
292 HeaderSource {
293 arch: self.arch().unwrap_or("").to_string(),
294 meta: self.metadata.iter().filter_map(|(k, v)| Some((k.clone(), value(v)?))).collect(),
295 dims: self.tensors.iter().map(|t| (t.name.clone(), t.dims.clone())).collect(),
296 names: self.tensors.iter().map(|t| t.name.clone()).collect(),
297 }
298 }
299
300 pub fn spec(&self) -> Result<crate::model::spec::ModelSpec, crate::model::spec::SpecError> {
303 crate::model::spec::ModelSpec::from_source(&self.source())
304 }
305
306 pub fn arch(&self) -> Option<&str> {
308 self.metadata.get("general.architecture")?.as_str()
309 }
310
311 pub fn meta_u64(&self, key: &str) -> Option<u64> {
313 self.metadata.get(&format!("{}.{key}", self.arch()?))?.as_u64()
314 }
315
316 fn require(&self, key: &str) -> Result<u64, DecodeRefusal> {
317 self.meta_u64(key)
318 .ok_or_else(|| DecodeRefusal::MissingMetadata(format!("{}.{key}", self.arch().unwrap_or("?"))))
319 }
320
321 pub fn tensor_type(&self, t: &TensorRow) -> Result<GgmlType, DecodeRefusal> {
323 GgmlType::from_id(t.type_id).ok_or_else(|| DecodeRefusal::UnknownType {
324 tensor: t.name.clone(),
325 id: t.type_id,
326 })
327 }
328
329 pub fn tensor_bytes(&self, t: &TensorRow) -> Result<u64, DecodeRefusal> {
331 let ty = self.tensor_type(t)?;
332 let elems: u64 = t.dims.iter().product();
333 ty.bytes(elems).ok_or_else(|| DecodeRefusal::RaggedBlock {
334 tensor: t.name.clone(),
335 elems,
336 block: ty.block_elems(),
337 })
338 }
339
340 pub fn byte_audit(&self) -> Result<(), String> {
344 let align = self
345 .metadata
346 .get("general.alignment")
347 .and_then(|v| v.as_u64())
348 .unwrap_or(32);
349 let mut order: Vec<&TensorRow> = self.tensors.iter().collect();
350 order.sort_by_key(|t| t.offset);
351 let size = |t: &TensorRow| self.tensor_bytes(t).map_err(|e| format!("{e:?}"));
352 for pair in order.windows(2) {
353 let (a, b) = (pair[0], pair[1]);
354 let (bytes, gap) = (size(a)?, b.offset - a.offset);
355 if !(bytes <= gap && gap < bytes + align) {
356 return Err(format!(
357 "`{}` is {bytes} bytes but the next tensor starts {gap} bytes later",
358 a.name
359 ));
360 }
361 }
362 let last = order.last().ok_or("an empty tensor table")?;
363 let end = self.data_offset + last.offset + size(last)?;
364 if end != self.file_bytes {
365 return Err(format!(
366 "the last tensor `{}` ends at byte {end}, the file at {}",
367 last.name, self.file_bytes
368 ));
369 }
370 Ok(())
371 }
372}
373
374#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
378pub enum WeightBytes {
379 AsStored,
381 PerWeight(f64),
383}
384
385#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
387pub struct Deployment {
388 pub ctx: u64,
390 pub weights: WeightBytes,
391 pub kv_bytes: f64,
393 pub state_bytes: f64,
395 pub activation_bytes: f64,
397 pub speculative: bool,
399}
400
401impl Deployment {
402 pub fn as_stored(ctx: u64) -> Deployment {
404 Deployment {
405 ctx,
406 weights: WeightBytes::AsStored,
407 kv_bytes: 2.0,
408 state_bytes: 4.0,
409 activation_bytes: 2.0,
410 speculative: false,
411 }
412 }
413}
414
415#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
419pub enum StageKind {
420 Embed,
422 Norm,
424 Project,
426 Attention,
428 Recurrence,
430 Router,
432 Head,
434}
435
436#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
438pub enum Split {
439 Any,
441 Heads { q: u64, kv: u64 },
443 Replicated,
445 One,
447}
448
449#[derive(Clone, Debug, PartialEq, Serialize)]
451pub struct Stage {
452 pub layer: Option<u32>,
454 pub kind: StageKind,
455 pub tensors: Vec<String>,
456 pub weight_bytes: f64,
458 pub weight_class: HeldClass,
461 pub state_read: f64,
463 pub state_write: f64,
465 pub ops: f64,
467 pub split: Split,
468 pub reduces: bool,
471 pub reduce_bytes: f64,
473}
474
475#[derive(Clone, Debug, PartialEq, Serialize)]
477pub struct DecodeStep {
478 pub deployment: Deployment,
479 pub stages: Vec<Stage>,
480 pub held: Vec<Held>,
483}
484
485#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
487pub struct Held {
488 pub class: HeldClass,
489 pub bytes: f64,
490 pub moved_per_token: f64,
491}
492
493#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
494pub enum HeldClass {
495 Weights,
496 EmbeddingTable,
497 KvCache,
498 RecurrentState,
499}
500
501#[derive(Clone, Debug, PartialEq, Serialize)]
503pub enum DecodeRefusal {
504 UnknownTensor(String),
506 UnknownType { tensor: String, id: u32 },
508 RaggedBlock { tensor: String, elems: u64, block: u64 },
510 MissingMetadata(String),
512 Unreadable(String),
514 NotResident { needed: f64, capacity: f64 },
516}
517
518#[derive(Clone, Copy, Debug, PartialEq, Eq)]
520enum Role {
521 Embed,
522 RopeFreqs,
523 OutputNorm,
524 Output,
525 PreMixerNorm,
526 MixerIn,
527 MixerParam,
528 MixerOut,
529 PostMixerNorm,
530 PreFfnNorm,
531 Router,
532 FfnIn,
533 FfnInExperts,
534 FfnOut,
535 FfnOutExperts,
536 PostFfnNorm,
537 DraftNorm,
538 DraftIn,
539 DraftHeadNorm,
540}
541
542fn block_role(key: &str) -> Option<Role> {
543 use Role::*;
544 Some(match key {
545 "attn_norm" => PreMixerNorm,
546 "attn_q" | "attn_k" | "attn_v" | "attn_qkv" | "attn_gate" | "ssm_alpha" | "ssm_beta" => MixerIn,
547 "attn_q_norm" | "attn_k_norm" | "ssm_norm" | "ssm_conv1d" | "ssm_a" | "ssm_dt" => MixerParam,
548 "attn_output" | "ssm_out" => MixerOut,
549 "post_attention_norm" => PostMixerNorm,
550 "ffn_norm" => PreFfnNorm,
551 "ffn_gate_inp" | "exp_probs_b" | "ffn_gate_inp_shexp" => Router,
552 "ffn_gate" | "ffn_up" | "ffn_gate_shexp" | "ffn_up_shexp" => FfnIn,
553 "ffn_gate_exps" | "ffn_up_exps" => FfnInExperts,
554 "ffn_down" | "ffn_down_shexp" => FfnOut,
555 "ffn_down_exps" => FfnOutExperts,
556 "post_ffw_norm" | "ffn_post_norm" => PostFfnNorm,
557 "nextn.enorm" | "nextn.hnorm" => DraftNorm,
558 "nextn.eh_proj" => DraftIn,
559 "nextn.shared_head_norm" => DraftHeadNorm,
560 _ => return None,
561 })
562}
563
564fn global_role(name: &str) -> Option<Role> {
565 Some(match name {
566 "token_embd.weight" => Role::Embed,
567 "rope_freqs.weight" => Role::RopeFreqs,
568 "output_norm.weight" => Role::OutputNorm,
569 "output.weight" => Role::Output,
570 _ => return None,
571 })
572}
573
574fn split_block_name(name: &str) -> Option<(u32, &str)> {
576 let rest = name.strip_prefix("blk.")?;
577 let (idx, tail) = rest.split_once('.')?;
578 let key = tail
579 .strip_suffix(".weight")
580 .or_else(|| tail.strip_suffix(".bias"))
581 .unwrap_or(tail);
582 Some((idx.parse().ok()?, key))
583}
584
585fn is_matrix_role(r: Role) -> bool {
587 matches!(
588 r,
589 Role::Embed | Role::Output | Role::MixerIn | Role::MixerOut | Role::FfnIn | Role::FfnInExperts | Role::FfnOut | Role::FfnOutExperts | Role::DraftIn
590 )
591}
592
593struct Reader<'a> {
594 h: &'a GgufHeader,
595 d: &'a Deployment,
596}
597
598impl Reader<'_> {
599 fn stored(&self, t: &TensorRow, role: Role) -> Result<(f64, f64), DecodeRefusal> {
601 let elems: u64 = t.dims.iter().product();
602 let bytes = match self.d.weights {
603 WeightBytes::PerWeight(b) if is_matrix_role(role) && t.dims.len() >= 2 => elems as f64 * b,
604 _ => self.h.tensor_bytes(t)? as f64,
605 };
606 Ok((bytes, elems as f64))
607 }
608}
609
610impl DecodeStep {
611 pub fn of(h: &GgufHeader, d: &Deployment) -> Result<DecodeStep, DecodeRefusal> {
613 let r = Reader { h, d };
614 let blocks = h.require("block_count")? as u32;
615 let drafts = h.meta_u64("nextn_predict_layers").unwrap_or(0) as u32;
616 let embd = h.require("embedding_length")?;
617 let heads = h.require("attention.head_count")?;
618 let kv_heads = h.require("attention.head_count_kv")?;
619 let key_len = h.meta_u64("attention.key_length").unwrap_or(embd / heads);
620 let value_len = h.meta_u64("attention.value_length").unwrap_or(embd / heads);
621 let routed = match (h.meta_u64("expert_count"), h.meta_u64("expert_used_count")) {
622 (Some(n), Some(k)) if n > 0 => k as f64 / n as f64,
623 _ => 1.0,
624 };
625
626 let mut globals: BTreeMap<&'static str, &TensorRow> = BTreeMap::new();
627 let mut per_block: BTreeMap<u32, Vec<(Role, &TensorRow)>> = BTreeMap::new();
628 for t in &h.tensors {
629 h.tensor_type(t)?;
630 if let Some(role) = global_role(&t.name) {
631 let key = match role {
632 Role::Embed => "embed",
633 Role::RopeFreqs => "rope",
634 Role::OutputNorm => "output_norm",
635 _ => "output",
636 };
637 globals.insert(key, t);
638 continue;
639 }
640 let Some((b, key)) = split_block_name(&t.name) else {
641 return Err(DecodeRefusal::UnknownTensor(t.name.clone()));
642 };
643 let Some(role) = block_role(key) else {
644 return Err(DecodeRefusal::UnknownTensor(t.name.clone()));
645 };
646 if b >= blocks {
647 return Err(DecodeRefusal::Unreadable(format!("`{}` is past block_count {blocks}", t.name)));
648 }
649 per_block.entry(b).or_default().push((role, t));
650 }
651
652 let mut stages = Vec::new();
653 let mut held: BTreeMap<HeldClass, (f64, f64)> = BTreeMap::new();
654 let mut hold = |class: HeldClass, bytes: f64, moved: f64| {
655 let e = held.entry(class).or_insert((0.0, 0.0));
656 e.0 += bytes;
657 e.1 += moved;
658 };
659 let mut weights_held = 0.0;
660 let embed = globals
661 .get("embed")
662 .ok_or_else(|| DecodeRefusal::Unreadable("no token_embd.weight".into()))?;
663 let (table_bytes, _) = r.stored(embed, Role::Embed)?;
664 let vocab = *embed.dims.get(1).ok_or_else(|| DecodeRefusal::Unreadable("a one-dimensional embedding".into()))?;
665 let mut embed_stage = stage(None, StageKind::Embed, Split::One);
666 embed_stage.tensors.push(embed.name.clone());
667 embed_stage.weight_bytes = table_bytes / vocab as f64;
668 let mut rope = globals.get("rope").copied();
670 hold(HeldClass::EmbeddingTable, table_bytes, 0.0);
671 stages.push(embed_stage);
672
673 let reduce_bytes = embd as f64 * d.activation_bytes;
674 for b in 0..blocks {
675 let draft = b >= blocks.saturating_sub(drafts);
676 if draft && !d.speculative {
677 continue;
678 }
679 let Some(tensors) = per_block.get(&b) else { continue };
680 if !draft {
681 if let Some((_, t)) = tensors.iter().find(|(role, _)| matches!(role, Role::DraftNorm | Role::DraftIn | Role::DraftHeadNorm)) {
682 return Err(DecodeRefusal::Unreadable(format!("`{}` is a draft tensor in block {b}, which is not a draft block", t.name)));
683 }
684 }
685 let of = |want: &[Role]| -> Vec<(Role, &TensorRow)> { tensors.iter().filter(|(role, _)| want.contains(role)).copied().collect() };
686 let has = |key: &str| tensors.iter().any(|(_, t)| split_block_name(&t.name).is_some_and(|(_, k)| k == key));
687 let recurrent = tensors.iter().any(|(_, t)| split_block_name(&t.name).is_some_and(|(_, k)| k.starts_with("ssm_")));
688 let attention = has("attn_q") || has("attn_k") || has("attn_qkv");
689
690 let push = |kind: StageKind, split: Split, reduces: bool, rows: Vec<(Role, &TensorRow)>, stages: &mut Vec<Stage>, weights_held: &mut f64| -> Result<(), DecodeRefusal> {
691 if rows.is_empty() {
692 return Ok(());
693 }
694 let mut s = stage(Some(b), kind, split);
695 for (role, t) in rows {
696 let (bytes, elems) = r.stored(t, role)?;
697 let share = if matches!(role, Role::FfnInExperts | Role::FfnOutExperts) { routed } else { 1.0 };
698 *weights_held += bytes;
699 s.weight_bytes += bytes * share;
700 if is_matrix_role(role) {
701 s.ops += 2.0 * elems * share;
702 }
703 s.tensors.push(t.name.clone());
704 }
705 s.reduces = reduces;
706 if reduces {
707 s.reduce_bytes = reduce_bytes;
708 }
709 stages.push(s);
710 Ok(())
711 };
712
713 if draft {
714 push(StageKind::Norm, Split::Replicated, false, of(&[Role::DraftNorm]), &mut stages, &mut weights_held)?;
715 push(StageKind::Project, Split::Any, false, of(&[Role::DraftIn]), &mut stages, &mut weights_held)?;
716 }
717 push(StageKind::Norm, Split::Replicated, false, of(&[Role::PreMixerNorm]), &mut stages, &mut weights_held)?;
718 push(StageKind::Project, Split::Any, false, of(&[Role::MixerIn]), &mut stages, &mut weights_held)?;
719
720 let mut mixer = if recurrent {
721 let v_heads = tensors
722 .iter()
723 .find(|(_, t)| split_block_name(&t.name).is_some_and(|(_, k)| k == "ssm_a"))
724 .and_then(|(_, t)| t.dims.first().copied())
725 .ok_or_else(|| DecodeRefusal::Unreadable(format!("block {b}: a recurrence without ssm_a")))?;
726 let head_v = tensors
727 .iter()
728 .find(|(_, t)| split_block_name(&t.name).is_some_and(|(_, k)| k == "ssm_norm"))
729 .and_then(|(_, t)| t.dims.first().copied())
730 .ok_or_else(|| DecodeRefusal::Unreadable(format!("block {b}: a recurrence without ssm_norm")))?;
731 let head_k = h.require("ssm.state_size")?;
732 let conv_kernel = h.require("ssm.conv_kernel")?;
733 let conv_channels = tensors
734 .iter()
735 .find(|(_, t)| split_block_name(&t.name).is_some_and(|(_, k)| k == "ssm_conv1d"))
736 .and_then(|(_, t)| t.dims.get(1).copied())
737 .ok_or_else(|| DecodeRefusal::Unreadable(format!("block {b}: a recurrence without ssm_conv1d")))?;
738 let state = (v_heads * head_k * head_v + (conv_kernel - 1) * conv_channels) as f64 * d.state_bytes;
739 let mut s = stage(Some(b), StageKind::Recurrence, Split::Heads { q: v_heads, kv: v_heads });
740 s.state_read = state;
741 s.state_write = state;
742 s.ops = 2.0 * (3 * v_heads * head_k * head_v + conv_kernel * conv_channels) as f64;
745 hold(HeldClass::RecurrentState, state, 2.0 * state);
746 s
747 } else if attention {
748 let per_token = (kv_heads * (key_len + value_len)) as f64 * d.kv_bytes;
749 let mut s = stage(Some(b), StageKind::Attention, Split::Heads { q: heads, kv: kv_heads });
750 s.state_read = per_token * d.ctx as f64;
751 s.state_write = per_token;
752 s.ops = 2.0 * (heads * d.ctx * (key_len + value_len)) as f64;
753 hold(HeldClass::KvCache, per_token * d.ctx as f64, s.state_read + s.state_write);
754 s
755 } else {
756 return Err(DecodeRefusal::Unreadable(format!("block {b}: neither attention nor a recurrence")));
757 };
758 let first_rotation = rope.filter(|_| mixer.kind == StageKind::Attention);
759 for (role, t) in of(&[Role::MixerParam]).into_iter().chain(first_rotation.map(|t| (Role::RopeFreqs, t))) {
760 let bytes = r.stored(t, role)?.0;
761 weights_held += bytes;
762 mixer.weight_bytes += bytes;
763 mixer.tensors.push(t.name.clone());
764 }
765 if first_rotation.is_some() {
766 rope = None;
767 }
768 stages.push(mixer);
769
770 push(StageKind::Project, Split::Any, true, of(&[Role::MixerOut]), &mut stages, &mut weights_held)?;
771 push(StageKind::Norm, Split::Replicated, false, of(&[Role::PostMixerNorm]), &mut stages, &mut weights_held)?;
772 push(StageKind::Norm, Split::Replicated, false, of(&[Role::PreFfnNorm]), &mut stages, &mut weights_held)?;
773 push(StageKind::Router, Split::Replicated, false, of(&[Role::Router]), &mut stages, &mut weights_held)?;
774 push(StageKind::Project, Split::Any, false, of(&[Role::FfnIn, Role::FfnInExperts]), &mut stages, &mut weights_held)?;
775 push(StageKind::Project, Split::Any, true, of(&[Role::FfnOut, Role::FfnOutExperts]), &mut stages, &mut weights_held)?;
776 push(StageKind::Norm, Split::Replicated, false, of(&[Role::PostFfnNorm]), &mut stages, &mut weights_held)?;
777 if draft {
778 push(StageKind::Norm, Split::Replicated, false, of(&[Role::DraftHeadNorm]), &mut stages, &mut weights_held)?;
779 }
780 }
781
782 if let Some(t) = rope {
783 return Err(DecodeRefusal::Unreadable(format!("`{}` with no attention block that rotates by it", t.name)));
784 }
785 let output_norm = globals
786 .get("output_norm")
787 .ok_or_else(|| DecodeRefusal::Unreadable("no output_norm.weight".into()))?;
788 let mut norm = stage(None, StageKind::Norm, Split::Replicated);
789 norm.weight_bytes = r.stored(output_norm, Role::OutputNorm)?.0;
790 weights_held += norm.weight_bytes;
791 norm.tensors.push(output_norm.name.clone());
792 stages.push(norm);
793
794 let (head_tensor, head_role) = match globals.get("output") {
796 Some(t) => (*t, Role::Output),
797 None => (*embed, Role::Embed),
798 };
799 let (head_bytes, head_elems) = r.stored(head_tensor, head_role)?;
800 let mut head = stage(None, StageKind::Head, Split::Any);
801 head.weight_bytes = head_bytes;
802 head.ops = 2.0 * head_elems;
803 head.tensors.push(head_tensor.name.clone());
804 head.reduces = true;
805 head.reduce_bytes = 8.0;
807 if head_role == Role::Embed {
810 head.weight_class = HeldClass::EmbeddingTable;
811 } else {
812 weights_held += head_bytes;
813 }
814 stages.push(head);
815
816 let moved = |class: HeldClass| -> f64 { stages.iter().filter(|s| s.weight_class == class).map(|s| s.weight_bytes).sum() };
817 let (weights_moved, table_moved) = (moved(HeldClass::Weights), moved(HeldClass::EmbeddingTable));
818 hold(HeldClass::Weights, weights_held, weights_moved);
819 hold(HeldClass::EmbeddingTable, 0.0, table_moved);
820 let held = held
821 .into_iter()
822 .map(|(class, (bytes, moved_per_token))| Held { class, bytes, moved_per_token })
823 .collect();
824 Ok(DecodeStep { deployment: *d, stages, held })
825 }
826
827 pub fn weight_bytes(&self) -> f64 {
828 self.stages.iter().map(|s| s.weight_bytes).sum()
829 }
830
831 pub fn state_read_bytes(&self) -> f64 {
832 self.stages.iter().map(|s| s.state_read).sum()
833 }
834
835 pub fn state_write_bytes(&self) -> f64 {
836 self.stages.iter().map(|s| s.state_write).sum()
837 }
838
839 pub fn ops(&self) -> f64 {
840 self.stages.iter().map(|s| s.ops).sum()
841 }
842
843 pub fn held_bytes(&self) -> f64 {
845 self.held.iter().map(|h| h.bytes).sum()
846 }
847}
848
849fn stage(layer: Option<u32>, kind: StageKind, split: Split) -> Stage {
850 Stage {
851 layer,
852 kind,
853 tensors: Vec::new(),
854 weight_bytes: 0.0,
855 weight_class: match kind {
856 StageKind::Embed => HeldClass::EmbeddingTable,
857 _ => HeldClass::Weights,
858 },
859 state_read: 0.0,
860 state_write: 0.0,
861 ops: 0.0,
862 split,
863 reduces: false,
864 reduce_bytes: 0.0,
865 }
866}
867
868#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
872pub enum TierKind {
873 Sram,
874 Hbm,
875 Gddr,
876 Lpddr,
877 Ddr,
878}
879
880#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
882pub struct Tier {
883 pub kind: TierKind,
884 pub capacity: Fact<f64>,
886 pub bandwidth: Fact<f64>,
888}
889
890#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
893pub struct Link {
894 pub bandwidth: Option<Fact<f64>>,
896 pub hop_latency: Option<Fact<f64>>,
898}
899
900#[derive(Clone, Debug, PartialEq, Serialize)]
902pub struct Chip {
903 pub name: &'static str,
904 pub tiers: Vec<Tier>,
906 pub matrix: Vec<(Precision, Fact<f64>)>,
908 pub stage_overhead: Option<Fact<f64>>,
911 pub link: Option<Link>,
912 pub power: Option<Fact<f64>>,
914 pub sources: &'static [&'static str],
915}
916
917const NV_RTX3060_PAGE: &str = "nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3060-3060ti";
918const GROQ_SPEC_SHEET: &str = "groq.com/product-spec-sheet-groqchip-processor";
919const CEREBRAS_LAUNCH: &str = "cerebras.ai/blog/introducing-cerebras-inference-ai-at-instant-speed";
920
921const RTX3060_SOURCES: &[&str] = &[
922 NV_RTX3060_PAGE,
923 "nvidia.com/content/PDF/nvidia-ampere-ga-102-gpu-architecture-whitepaper-v2.pdf",
924];
925const A100_SOURCES: &[&str] = &[super::roofs::NV_A100_PAGE];
926const H100_SXM_SOURCES: &[&str] = &[super::roofs::NV_H100_PAGE];
927const H100_NVL_SOURCES: &[&str] = &[super::roofs::NV_H100_PAGE, super::roofs::NV_H100_NVL_BRIEF];
928const B200_SOURCES: &[&str] = &[super::roofs::NV_DGX_B200_PAGE, super::roofs::NV_HGX_PAGE];
929const GROQ_SOURCES: &[&str] = &[GROQ_SPEC_SHEET];
930const CEREBRAS_SOURCES: &[&str] = &[CEREBRAS_LAUNCH];
931
932fn gpu(
935 name: &'static str,
936 roofs: Roofs,
937 kind: TierKind,
938 capacity: Fact<f64>,
939 link: Option<Link>,
940 power: Option<Fact<f64>>,
941 sources: &'static [&'static str],
942) -> Chip {
943 let dram = roofs
944 .mem(MemLevel::Dram, StreamMix::Any)
945 .expect("a GPU roofs preset prices its memory");
946 let per_second = |rate: &Fact<Rate>| -> Fact<f64> {
947 let c = roofs.per_second(rate);
948 Fact { value: c.value, provenance: c.provenance, cite: rate.cite }
949 };
950 Chip {
951 name,
952 tiers: vec![Tier { kind, capacity, bandwidth: per_second(&dram.rate) }],
953 matrix: roofs
954 .compute
955 .iter()
956 .filter(|c| c.engine == Engine::Matrix)
957 .map(|c| (c.precision, per_second(&c.ops)))
958 .collect(),
959 stage_overhead: None,
960 link,
961 power,
962 sources,
963 }
964}
965
966fn nvlink(bytes_per_s: f64, cite: &'static str) -> Option<Link> {
967 Some(Link {
968 bandwidth: Some(Fact::published(bytes_per_s, cite)),
969 hop_latency: None,
970 })
971}
972
973impl Chip {
974 pub fn rtx3060() -> Chip {
976 Chip {
977 stage_overhead: Some(Fact::measured(
978 0.559e-6,
979 "benchmarks/silicon/decode/gpu_stage_overhead.json: graph_node_us_median 0.559 — the slope of a CUDA graph's \
980 replay time over the empty kernel nodes it chains (scripts/accel/graph_node_overhead.cu), three runs on an idle GPU",
981 )),
982 ..gpu(
983 "rtx3060",
984 Roofs::rtx3060(),
985 TierKind::Gddr,
986 Fact::published(12e9, "nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3060-3060ti: RTX 3060 \"12 GB GDDR6\""),
987 None,
988 None,
989 RTX3060_SOURCES,
990 )
991 }
992 }
993
994 pub fn a100_sxm4_80gb() -> Chip {
995 gpu(
996 "a100-sxm4-80gb",
997 Roofs::a100_sxm4_80gb(),
998 TierKind::Hbm,
999 Fact::published(80e9, "nvidia.com/en-us/data-center/a100: A100 80GB SXM \"GPU Memory 80GB HBM2e\""),
1000 nvlink(600e9, "nvidia.com/en-us/data-center/a100: A100 80GB SXM \"Interconnect NVLink: 600 GB/s\""),
1001 Some(Fact::published(400.0, "nvidia.com/en-us/data-center/a100: A100 80GB SXM \"Max Thermal Design Power (TDP) 400W\"")),
1002 A100_SOURCES,
1003 )
1004 }
1005
1006 pub fn h100_sxm() -> Chip {
1007 gpu(
1008 "h100-sxm",
1009 Roofs::h100_sxm(),
1010 TierKind::Hbm,
1011 Fact::published(80e9, "nvidia.com/en-us/data-center/h100: H100 SXM \"GPU Memory 80GB\""),
1012 nvlink(900e9, "nvidia.com/en-us/data-center/h100: H100 SXM \"Interconnect NVIDIA NVLink: 900GB/s\""),
1013 Some(Fact::published(700.0, "nvidia.com/en-us/data-center/h100: H100 SXM \"Max Thermal Design Power (TDP) Up to 700W (configurable)\"")),
1014 H100_SXM_SOURCES,
1015 )
1016 }
1017
1018 pub fn h100_nvl() -> Chip {
1019 gpu(
1020 "h100-nvl",
1021 Roofs::h100_nvl(),
1022 TierKind::Hbm,
1023 Fact::published(94e9, "nvidia.com/content/dam/en-zz/Solutions/Data-Center/h100/PB-11773-001_v01.pdf: Table 2 \"Memory size 94 GB\""),
1024 nvlink(600e9, "nvidia.com/en-us/data-center/h100: H100 NVL \"Interconnect NVIDIA NVLink: 600GB/s\""),
1025 Some(Fact::published(400.0, "nvidia.com/content/dam/en-zz/Solutions/Data-Center/h100/PB-11773-001_v01.pdf: Table 1 \"Total board power … 400 W maximum (default)\"")),
1026 H100_NVL_SOURCES,
1027 )
1028 }
1029
1030 pub fn b200() -> Chip {
1032 gpu(
1033 "b200",
1034 Roofs::b200(),
1035 TierKind::Hbm,
1036 Fact::derived(1440e9 / 8.0, "nvidia.com/en-us/data-center/dgx-b200: \"8x NVIDIA Blackwell GPUs\", \"1,440 GB total, 64 TB/s HBM3e bandwidth\"; 1,440 GB over 8 GPUs"),
1037 nvlink(1.8e12, "nvidia.com/en-us/data-center/hgx: HGX B200 \"NVIDIA NVLink GPU-to-GPU Bandwidth 1.8 TB/s\""),
1038 None,
1039 B200_SOURCES,
1040 )
1041 }
1042
1043 pub fn groq_lpu_v1() -> Chip {
1046 Chip {
1047 name: "groq-lpu-v1",
1048 tiers: vec![Tier {
1049 kind: TierKind::Sram,
1050 capacity: Fact::published(230e6, "groq.com/product-spec-sheet-groqchip-processor (GroqChip Processor v1.5): \"230 MB SRAM per chip\""),
1051 bandwidth: Fact::published(80e12, "groq.com/product-spec-sheet-groqchip-processor (GroqChip Processor v1.5): \"Up to 80 TB/s on-die memory bandwidth\""),
1052 }],
1053 matrix: vec![
1054 (Precision::Int8, Fact::published(750e12, "groq.com/product-spec-sheet-groqchip-processor (GroqChip Processor v1.5): \"Up to 750 TOPs, 188 TFLOPs (INT8, FP16 @900 MHz)\"")),
1055 (Precision::Fp16, Fact::published(188e12, "groq.com/product-spec-sheet-groqchip-processor (GroqChip Processor v1.5): \"Up to 750 TOPs, 188 TFLOPs (INT8, FP16 @900 MHz)\"")),
1056 ],
1057 stage_overhead: None,
1058 link: Some(Link { bandwidth: None, hop_latency: None }),
1059 power: Some(Fact::published(215.0, "groq.com/product-spec-sheet-groqchip-processor (GroqChip Processor v1.5): \"Power Max: 300W; TDP: 215W; Average: 185W\"")),
1060 sources: GROQ_SOURCES,
1061 }
1062 }
1063
1064 pub fn cerebras_wse3() -> Chip {
1066 Chip {
1067 name: "cerebras-wse3",
1068 tiers: vec![Tier {
1069 kind: TierKind::Sram,
1070 capacity: Fact::published(44e9, "cerebras.ai/blog/introducing-cerebras-inference-ai-at-instant-speed: \"44GB of SRAM on a single chip\""),
1071 bandwidth: Fact::published(21e15, "cerebras.ai/blog/introducing-cerebras-inference-ai-at-instant-speed: \"21 petabytes/s of aggregate memory bandwidth\""),
1072 }],
1073 matrix: Vec::new(),
1074 stage_overhead: None,
1075 link: Some(Link { bandwidth: None, hop_latency: None }),
1076 power: None,
1077 sources: CEREBRAS_SOURCES,
1078 }
1079 }
1080
1081 pub fn catalog() -> Vec<Chip> {
1083 vec![
1084 Chip::rtx3060(),
1085 Chip::a100_sxm4_80gb(),
1086 Chip::h100_sxm(),
1087 Chip::h100_nvl(),
1088 Chip::b200(),
1089 Chip::groq_lpu_v1(),
1090 Chip::cerebras_wse3(),
1091 super::fpga_decode::zcu104(),
1092 super::d5005::chip(),
1093 ]
1094 }
1095
1096 pub fn by_name(name: &str) -> Option<Chip> {
1097 Chip::catalog().into_iter().find(|c| c.name == name)
1098 }
1099
1100 pub fn matrix_rate(&self, p: Precision) -> Option<Fact<f64>> {
1102 self.matrix.iter().find(|(q, _)| *q == p).map(|(_, f)| *f)
1103 }
1104
1105 pub fn facts(&self) -> Vec<(String, Provenance, &'static str)> {
1107 let mut out = Vec::new();
1108 for t in &self.tiers {
1109 out.push((format!("tier.{:?}.capacity", t.kind), t.capacity.provenance, t.capacity.cite));
1110 out.push((format!("tier.{:?}.bandwidth", t.kind), t.bandwidth.provenance, t.bandwidth.cite));
1111 }
1112 for (p, f) in &self.matrix {
1113 out.push((format!("matrix.{p:?}"), f.provenance, f.cite));
1114 }
1115 if let Some(f) = &self.stage_overhead {
1116 out.push(("stage_overhead".into(), f.provenance, f.cite));
1117 }
1118 if let Some(l) = &self.link {
1119 if let Some(f) = &l.bandwidth {
1120 out.push(("link.bandwidth".into(), f.provenance, f.cite));
1121 }
1122 if let Some(f) = &l.hop_latency {
1123 out.push(("link.hop_latency".into(), f.provenance, f.cite));
1124 }
1125 }
1126 if let Some(f) = &self.power {
1127 out.push(("power".into(), f.provenance, f.cite));
1128 }
1129 out
1130 }
1131}
1132
1133#[derive(Clone, Debug, PartialEq, Serialize)]
1135pub struct Machine {
1136 pub chip: Chip,
1137 pub chips: u32,
1138}
1139
1140impl Machine {
1141 pub fn of(chip: Chip, chips: u32) -> Machine {
1142 assert!(chips >= 1, "a machine has a chip");
1143 Machine { chip, chips }
1144 }
1145}
1146
1147#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
1151pub enum Bind {
1152 Memory,
1153 Compute,
1154 Collective,
1155 Overhead,
1156}
1157
1158#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
1160pub struct StageCost {
1161 pub ways: u32,
1163 pub memory_s: f64,
1164 pub compute_s: f64,
1165 pub collective_floor_s: f64,
1167 pub collective_s: Option<f64>,
1169 pub overhead_s: Option<f64>,
1170}
1171
1172#[derive(Clone, Debug, PartialEq, Serialize)]
1174pub struct DecodeCost {
1175 pub machine: &'static str,
1176 pub chips: u32,
1177 pub stages: Vec<StageCost>,
1178 pub floor_s: f64,
1181 pub floor_binds: Bind,
1182 pub time_s: Option<f64>,
1185 pub collectives: usize,
1187 pub residency: Vec<(TierKind, f64)>,
1189 pub provenance: Provenance,
1191}
1192
1193impl DecodeCost {
1194 pub fn floor_tokens_per_s(&self) -> f64 {
1195 1.0 / self.floor_s
1196 }
1197
1198 pub fn tokens_per_s(&self) -> Option<f64> {
1199 self.time_s.map(|t| 1.0 / t)
1200 }
1201}
1202
1203fn collective(p: u32, bytes: f64, link: Option<&Link>) -> (f64, Option<f64>) {
1206 if p <= 1 {
1207 return (0.0, Some(0.0));
1208 }
1209 let lat = link.and_then(|l| l.hop_latency).map(|f| f.value);
1210 let bw = link.and_then(|l| l.bandwidth).map(|f| f.value);
1211 let cost = |lat: f64, inv_bw: f64| -> f64 {
1212 let steps = (p as f64).log2().ceil();
1213 let doubling = steps * (lat + bytes * inv_bw);
1214 let ring = 2.0 * (p as f64 - 1.0) * (lat + bytes / p as f64 * inv_bw);
1215 doubling.min(ring)
1216 };
1217 let floor = cost(lat.unwrap_or(0.0), bw.map_or(0.0, |b| 1.0 / b));
1218 let declared = match (lat, bw) {
1219 (Some(l), Some(b)) => Some(cost(l, 1.0 / b)),
1220 _ => None,
1221 };
1222 (floor, declared)
1223}
1224
1225pub fn price(step: &DecodeStep, m: &Machine, precision: Precision) -> Result<DecodeCost, DecodeRefusal> {
1227 let chip = &m.chip;
1228 let p = m.chips;
1229
1230 let mut classes = step.held.clone();
1232 classes.sort_by(|a, b| (b.moved_per_token / b.bytes.max(1.0)).total_cmp(&(a.moved_per_token / a.bytes.max(1.0))));
1233 let needed = step.held_bytes();
1234 let capacity: f64 = chip.tiers.iter().map(|t| t.capacity.value).sum::<f64>() * p as f64;
1235 if needed > capacity {
1236 return Err(DecodeRefusal::NotResident { needed, capacity });
1237 }
1238 let mut free: Vec<f64> = chip.tiers.iter().map(|t| t.capacity.value * p as f64).collect();
1239 let mut share: BTreeMap<HeldClass, Vec<f64>> = BTreeMap::new();
1241 for c in &classes {
1242 let mut left = c.bytes;
1243 let mut row = vec![0.0; chip.tiers.len()];
1244 for (i, f) in free.iter_mut().enumerate() {
1245 let take = left.min(*f);
1246 *f -= take;
1247 left -= take;
1248 row[i] = if c.bytes > 0.0 { take / c.bytes } else { (i == 0) as u8 as f64 };
1249 }
1250 share.insert(c.class, row);
1251 }
1252 let residency = chip
1253 .tiers
1254 .iter()
1255 .zip(free.iter())
1256 .map(|(t, f)| (t.kind, (t.capacity.value * p as f64 - f) / p as f64))
1257 .collect();
1258 let seconds = |bytes: f64, class: HeldClass| -> f64 {
1259 share[&class]
1260 .iter()
1261 .zip(&chip.tiers)
1262 .map(|(s, t)| bytes * s / t.bandwidth.value)
1263 .sum()
1264 };
1265
1266 let rate = chip.matrix_rate(precision);
1267 let link = chip.link.as_ref();
1268 let mut provenance = chip.tiers.iter().map(|t| t.bandwidth.provenance).max().unwrap_or(Provenance::Published);
1269 if let Some(r) = rate {
1270 provenance = provenance.max(r.provenance);
1271 }
1272 let mut costs = Vec::with_capacity(step.stages.len());
1273 let mut collectives = 0;
1274 for s in &step.stages {
1275 let (ways, state_ways) = match s.split {
1276 Split::Any => (p, p),
1277 Split::Heads { q, kv } => (p.min(q as u32), p.min(kv as u32)),
1278 Split::Replicated | Split::One => (1, 1),
1279 };
1280 let state_class = match s.kind {
1281 StageKind::Recurrence => HeldClass::RecurrentState,
1282 _ => HeldClass::KvCache,
1283 };
1284 let memory_s = seconds(s.weight_bytes / ways as f64, s.weight_class)
1285 + if s.state_read + s.state_write > 0.0 {
1286 seconds((s.state_read + s.state_write) / state_ways as f64, state_class)
1287 } else {
1288 0.0
1289 };
1290 let compute_s = rate.map_or(0.0, |r| s.ops / ways as f64 / r.value);
1291 let (collective_floor_s, collective_s) = if s.reduces && ways > 1 {
1292 collectives += 1;
1293 collective(ways, s.reduce_bytes, link)
1294 } else {
1295 (0.0, Some(0.0))
1296 };
1297 costs.push(StageCost {
1298 ways,
1299 memory_s,
1300 compute_s,
1301 collective_floor_s,
1302 collective_s,
1303 overhead_s: chip.stage_overhead.map(|f| f.value),
1304 });
1305 }
1306
1307 let memory: f64 = costs.iter().map(|c| c.memory_s).sum();
1308 let compute: f64 = costs.iter().map(|c| c.compute_s).sum();
1309 let collective_sum: f64 = costs.iter().map(|c| c.collective_floor_s).sum();
1310 let (floor_s, floor_binds) = [(memory, Bind::Memory), (compute, Bind::Compute), (collective_sum, Bind::Collective)]
1311 .into_iter()
1312 .max_by(|a, b| a.0.total_cmp(&b.0))
1313 .expect("three terms");
1314 let time_s = costs
1315 .iter()
1316 .map(|c| Some(c.memory_s.max(c.compute_s) + c.collective_s? + c.overhead_s?))
1317 .sum::<Option<f64>>();
1318 if time_s.is_some() {
1319 if let Some(o) = chip.stage_overhead {
1320 provenance = provenance.max(o.provenance);
1321 }
1322 if p > 1 {
1323 if let Some(l) = link {
1324 for f in [l.bandwidth, l.hop_latency].into_iter().flatten() {
1325 provenance = provenance.max(f.provenance);
1326 }
1327 }
1328 }
1329 }
1330 Ok(DecodeCost {
1331 machine: chip.name,
1332 chips: p,
1333 stages: costs,
1334 floor_s,
1335 floor_binds,
1336 time_s,
1337 collectives,
1338 residency,
1339 provenance,
1340 })
1341}