Skip to main content

logicaffeine_compile/spatial/
decode.rs

1//! The cost of one token: batch-1 decode, priced on a machine from the checkpoint itself.
2//!
3//! ★ AT BATCH ONE, EVERY TOKEN REREADS EVERY WEIGHT. A decode step multiplies one activation vector through
4//! every projection of the model, so its arithmetic intensity is one multiply-add per weight byte read and the
5//! matrix engines idle: the step is bounded by where the weights live and how fast that memory streams, and by
6//! how long its chain of dependent stages takes to walk. That is the regime a GPU is worst at and an
7//! SRAM-resident dataflow machine is built for, so it is the regime this module prices for both, by one rule.
8//!
9//! The step is read off a GGUF header ([`GgufHeader`]): every tensor's bytes from ggml's block arithmetic
10//! ([`GgmlType`]), every tensor's role from llama.cpp's tensor names, the KV cache and the recurrent state from
11//! the header's metadata. Nothing is per model: a block's mixer is attention when it carries attention
12//! projections and a recurrence when it carries `ssm_*` tensors; a tensor no role names is refused by name.
13//!
14//! The machine is facts with sources ([`Chip`]), by the rule the roofs keep: a GPU's rates are its roofs
15//! preset's, its capacities and links its vendor's table's. What the record does not give is absent, never
16//! guessed — an undeclared per-stage overhead or link latency leaves the FLOOR priced (it is a true lower
17//! bound with the term at zero) and the PREDICTION refused.
18//!
19//! [`price`] lays the step's stages onto the chips Megatron's way — projections split, attention split by
20//! heads, norms replicated, an all-reduce after each block's attention output and FFN down projection, an
21//! arg-max across vocabulary shards at the head — and returns both numbers: the floor no implementation can
22//! beat (the largest of the memory, compute and collective sums), and, where every term is declared, the
23//! prediction (each stage's roofline plus its collective and overhead, summed along the chain).
24
25use 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
34// ── ggml's storage types ────────────────────────────────────────────────────────────────────────────
35
36/// Where ggml's block arithmetic is read from.
37pub 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/// A ggml tensor storage type: the id GGUF stores and the block it packs elements into.
42#[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    /// Every type ggml currently defines. Ids 4, 5, 31–33 and 36–38 are retired and absent.
84    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    /// `(id, elements per block, bytes per block)`, from [`GGML_TYPES_SOURCE`].
123    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    /// The type GGUF stores as `id`, or `None` for a retired or unknown id — never a guess.
177    pub fn from_id(id: u32) -> Option<GgmlType> {
178        GgmlType::ALL.into_iter().find(|t| t.id() == id)
179    }
180
181    /// Bytes `elems` elements take, or `None` when they do not fill whole blocks.
182    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// ── The header ──────────────────────────────────────────────────────────────────────────────────────
188
189/// One row of a GGUF tensor table.
190#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
191pub struct TensorRow {
192    pub name: String,
193    /// Dimensions, innermost first (ggml's order): a projection is `[in, out]`, an expert stack `[in, out, n]`.
194    pub dims: Vec<u64>,
195    #[serde(rename = "type")]
196    pub type_id: u32,
197    /// Byte offset of the tensor's data from [`GgufHeader::data_offset`].
198    pub offset: u64,
199}
200
201/// A GGUF header: the metadata and the whole tensor table, and where in the file the tensor data lives.
202#[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
215/// A header ledger read as a checkpoint's description: its metadata as GGUF values — an integer unsigned, or signed
216/// when negative; a decimal that is exactly a binary32 one, as the converters store metadata decimals, any other
217/// binary64; an array of its first item's type; a value the ledger summarized (a vocabulary as `<n strings>`) the
218/// summary's string — and its tensor directory.
219pub 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
241/// A header as a checkpoint's weights: every tensor's format and dims and none of its bytes, what lays the checkpoint
242/// out on the card and lowers its step.
243impl 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    /// A header ledger (`benchmarks/silicon/decode/headers/*.json`, written by
259    /// `scripts/accel/gguf_remote_header.py --tensors`).
260    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    /// The header as a checkpoint's description ([`crate::model::spec::SpecSource`]).
267    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    /// The architecture the header describes, read as [`crate::model::spec::ModelSpec::from_gguf`] reads its
301    /// checkpoint's.
302    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    /// `general.architecture` — the prefix every architecture key carries.
307    pub fn arch(&self) -> Option<&str> {
308        self.metadata.get("general.architecture")?.as_str()
309    }
310
311    /// The architecture key `{arch}.{key}` as an unsigned integer.
312    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    /// The storage type of `t`.
322    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    /// Bytes `t` occupies in the file.
330    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    /// Holds the tensor table to the file: in offset order each tensor's bytes fill the gap to the next up to
341    /// the alignment padding, and the last one ends exactly where the file does. A wrong block size, a wrong
342    /// type id or a missing tensor cannot pass.
343    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// ── The deployment ──────────────────────────────────────────────────────────────────────────────────
375
376/// How the matrix weights are stored for the run.
377#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
378pub enum WeightBytes {
379    /// As the checkpoint stores them, block scales included.
380    AsStored,
381    /// Re-quantized to this many bytes per weight (fp8 = 1, bf16 = 2); vectors stay as stored.
382    PerWeight(f64),
383}
384
385/// What a deployment of a checkpoint holds per token.
386#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
387pub struct Deployment {
388    /// The context the KV cache has reached.
389    pub ctx: u64,
390    pub weights: WeightBytes,
391    /// Bytes per KV-cache element.
392    pub kv_bytes: f64,
393    /// Bytes per recurrent-state element.
394    pub state_bytes: f64,
395    /// Bytes per activation element a reduction across chips carries.
396    pub activation_bytes: f64,
397    /// Whether the speculative (multi-token-prediction) blocks run as part of the step.
398    pub speculative: bool,
399}
400
401impl Deployment {
402    /// The checkpoint as stored, with llama.cpp's defaults: an f16 KV cache and an f32 recurrent state.
403    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// ── The step ────────────────────────────────────────────────────────────────────────────────────────
416
417/// What a stage of the chain does.
418#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
419pub enum StageKind {
420    /// One embedding row.
421    Embed,
422    /// A normalization over the whole hidden vector: a full-vector reduction, so a dependent step.
423    Norm,
424    /// Matrix-vector products that share one input vector.
425    Project,
426    /// Scores and the weighted sum over the KV cache.
427    Attention,
428    /// A linear recurrence over a carried state (Gated DeltaNet, Mamba-class).
429    Recurrence,
430    /// The expert router: its top-k picks which experts the next stage reads.
431    Router,
432    /// The vocabulary projection and the arg-max over it.
433    Head,
434}
435
436/// How far a stage's work divides across chips.
437#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
438pub enum Split {
439    /// Rows or columns of its projections: any number of chips.
440    Any,
441    /// By heads: its arithmetic over `q` query heads, its carried state over `kv` heads.
442    Heads { q: u64, kv: u64 },
443    /// Every chip does all of it (a norm, a router).
444    Replicated,
445    /// One chip does it (an embedding row).
446    One,
447}
448
449/// One dependent stage of the decode chain.
450#[derive(Clone, Debug, PartialEq, Serialize)]
451pub struct Stage {
452    /// The block, or `None` for the embedding and the head.
453    pub layer: Option<u32>,
454    pub kind: StageKind,
455    pub tensors: Vec<String>,
456    /// Weight bytes the stage reads per token.
457    pub weight_bytes: f64,
458    /// The held bytes those weight bytes are read from, and so the tiers they stream from: the embedding table for
459    /// the embedding row and for a head tied to the table, the weights for every other stage.
460    pub weight_class: HeldClass,
461    /// Carried-state bytes read per token: the KV cache, a recurrent state.
462    pub state_read: f64,
463    /// Carried-state bytes written per token.
464    pub state_write: f64,
465    /// Matrix-engine operations per token (a multiply-add counts two).
466    pub ops: f64,
467    pub split: Split,
468    /// Whether the stage's output is summed across the chips it is split over (Megatron's row-split
469    /// projections) or reduced to one answer (the head's arg-max).
470    pub reduces: bool,
471    /// Bytes that reduction carries.
472    pub reduce_bytes: f64,
473}
474
475/// What one token of a deployment reads, writes and computes, as the chain of its dependent stages.
476#[derive(Clone, Debug, PartialEq, Serialize)]
477pub struct DecodeStep {
478    pub deployment: Deployment,
479    pub stages: Vec<Stage>,
480    /// Bytes the deployment holds resident: every weight the step reads (the whole embedding table, not one
481    /// row), the KV cache at the deployment's context and the recurrent state, by how often a token moves them.
482    pub held: Vec<Held>,
483}
484
485/// A class of resident bytes and the bytes of it one token moves: what decides which memory tier keeps it.
486#[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/// Why a step or a price could not be computed.
502#[derive(Clone, Debug, PartialEq, Serialize)]
503pub enum DecodeRefusal {
504    /// A tensor whose name no role names.
505    UnknownTensor(String),
506    /// A storage type id ggml does not define (or retired).
507    UnknownType { tensor: String, id: u32 },
508    /// A tensor whose element count does not fill whole blocks of its type.
509    RaggedBlock { tensor: String, elems: u64, block: u64 },
510    /// A metadata key the step needs.
511    MissingMetadata(String),
512    /// A block whose tensors name no mixer, or a shape the role table cannot read.
513    Unreadable(String),
514    /// The held bytes exceed every tier of every chip.
515    NotResident { needed: f64, capacity: f64 },
516}
517
518/// A tensor's job, from llama.cpp's tensor names.
519#[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
574/// `blk.7.attn_q.weight` → `(7, "attn_q")`.
575fn 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
585/// The roles whose weights a matrix engine reads, and so the ones a re-quantization re-stores.
586fn 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    /// Bytes `t` takes in the deployment, and its element count.
600    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    /// The step of `d` over the checkpoint `h` describes.
612    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        // RoPE's frequency table is a weight every attention block rotates by: a token reads it once, with the first.
669        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                // Decay, rank-one update and readout: one multiply-add per state element each; the depthwise
743                // convolution one per tap per channel.
744                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        // A checkpoint without `output.weight` ties its head to the embedding table: the head reads all of it.
795        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        // The arg-max across vocabulary shards carries a value and an index.
806        head.reduce_bytes = 8.0;
807        // A tied head reads the embedding table, which is already held: its bytes stream from wherever the table
808        // lives.
809        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    /// Every byte the deployment keeps resident.
844    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// ── The machine ─────────────────────────────────────────────────────────────────────────────────────
869
870/// A kind of memory a chip holds bytes in.
871#[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/// One memory tier of a chip: how much it holds and how fast it streams.
881#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
882pub struct Tier {
883    pub kind: TierKind,
884    /// Bytes.
885    pub capacity: Fact<f64>,
886    /// Bytes per second.
887    pub bandwidth: Fact<f64>,
888}
889
890/// The chip-to-chip network. Either rate may be undeclared: the floor then prices it at zero, which is a true
891/// lower bound, and the prediction refuses.
892#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
893pub struct Link {
894    /// Bytes per second one chip injects.
895    pub bandwidth: Option<Fact<f64>>,
896    /// Seconds one hop of a collective takes, end to end.
897    pub hop_latency: Option<Fact<f64>>,
898}
899
900/// A chip as a decode sees it.
901#[derive(Clone, Debug, PartialEq, Serialize)]
902pub struct Chip {
903    pub name: &'static str,
904    /// Fastest first.
905    pub tiers: Vec<Tier>,
906    /// Dense matrix-engine operations per second, by operand precision.
907    pub matrix: Vec<(Precision, Fact<f64>)>,
908    /// What a dependent stage costs beyond its roofline: a kernel launch, a pipeline sync. `None` is
909    /// undeclared, NOT zero.
910    pub stage_overhead: Option<Fact<f64>>,
911    pub link: Option<Link>,
912    /// Watts.
913    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
932/// A GPU whose rates are its roofs preset's: one memory tier and the matrix roofs, converted per second with
933/// the roof's own citation.
934fn 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    /// The local RTX 3060, the one GPU whose per-stage overhead is measured here.
975    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    /// One GPU of a DGX/HGX B200.
1031    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    /// The first-generation GroqChip: weights live in its on-die SRAM. Its chip-to-chip links are counted but
1044    /// their rates are not published.
1045    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    /// Cerebras WSE-3, one wafer. Its matrix rate is published without a precision, so it is not priced.
1065    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    /// Every chip a decode is priced on.
1082    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    /// The dense matrix rate at `p`.
1101    pub fn matrix_rate(&self, p: Precision) -> Option<Fact<f64>> {
1102        self.matrix.iter().find(|(q, _)| *q == p).map(|(_, f)| *f)
1103    }
1104
1105    /// Every fact the chip carries, with the path a lock names it by.
1106    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/// A number of identical chips running one step, tensor-parallel.
1134#[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// ── The price ───────────────────────────────────────────────────────────────────────────────────────
1148
1149/// The term that binds.
1150#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
1151pub enum Bind {
1152    Memory,
1153    Compute,
1154    Collective,
1155    Overhead,
1156}
1157
1158/// One stage's terms, in seconds, on the machine it was priced for.
1159#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
1160pub struct StageCost {
1161    /// Over how many chips the stage's arithmetic divides.
1162    pub ways: u32,
1163    pub memory_s: f64,
1164    pub compute_s: f64,
1165    /// The stage's collective with undeclared terms at zero: what the floor counts.
1166    pub collective_floor_s: f64,
1167    /// The collective with every term declared, or `None`.
1168    pub collective_s: Option<f64>,
1169    pub overhead_s: Option<f64>,
1170}
1171
1172/// One token of a step on a machine.
1173#[derive(Clone, Debug, PartialEq, Serialize)]
1174pub struct DecodeCost {
1175    pub machine: &'static str,
1176    pub chips: u32,
1177    pub stages: Vec<StageCost>,
1178    /// No implementation decodes a token faster: the largest of the summed memory, compute and collective
1179    /// terms, with undeclared terms at zero.
1180    pub floor_s: f64,
1181    pub floor_binds: Bind,
1182    /// Each stage's roofline plus its collective and its overhead, summed along the chain — `None` while any
1183    /// term it needs is undeclared.
1184    pub time_s: Option<f64>,
1185    /// Reductions across chips per token.
1186    pub collectives: usize,
1187    /// Of each tier, the share of the held bytes it keeps, per chip.
1188    pub residency: Vec<(TierKind, f64)>,
1189    /// The weakest fact the price rests on.
1190    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
1203/// The fastest collective of `bytes` over `p` chips the link allows: recursive doubling (`⌈log₂ p⌉` hops,
1204/// each carrying the whole vector) or a ring (`2(p−1)` hops of a `1/p` share). Undeclared rates count zero.
1205fn 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
1225/// Prices `step` on `m`, with matrix arithmetic at `precision`.
1226pub fn price(step: &DecodeStep, m: &Machine, precision: Precision) -> Result<DecodeCost, DecodeRefusal> {
1227    let chip = &m.chip;
1228    let p = m.chips;
1229
1230    // Residency: fill the fastest tier first with what a token moves the most of per byte held.
1231    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    // For each class, the share of it each tier holds.
1240    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}