Skip to main content

logicaffeine_compile/spatial/
decode_chip.rs

1//! The decode chip — what an SRAM-resident LOGOS system must achieve to decode one user faster than GPUs can.
2//!
3//! [`super::decode`] prices a token on any machine. This module turns the question around: given a checkpoint, a
4//! GPU deployment's FLOOR (its memory roofline with every overhead at zero — the most a GPU could ever do) and a
5//! speedup, it asks what a system of LOGOS chips must achieve, and answers in the units a chip is designed in.
6//!
7//! ★ AT BATCH ONE ON SRAM, BANDWIDTH IS NOT WHAT BINDS. Weights held in on-die SRAM across enough chips stream in
8//! microseconds; what remains is the CHAIN — a token walks every block's dependent stages (norm, projection,
9//! mixer, projection, all-reduce, …), and each one costs its on-chip latency (the input vector out to every
10//! tile, the partial sums back) plus, where it reduces across chips, its collective's hops. So the answer is a
11//! pair of latency budgets: the most one dependent stage may take on chip, and the most one chip-to-chip hop
12//! may take, for the system to beat the GPU floor by the speedup. [`demonstrated_stage_latency`] reads the same
13//! quantity back out of the vendors' published per-user rates, so a budget can be set against what an SRAM
14//! machine has actually shown.
15//!
16//! The chip is a design point ([`DecodeChipDesign`]): a die area, the share of it that is SRAM, the SRAM per
17//! tile and the on-chip network's shape are CHOICES; the node's SRAM density, read width and clock are FACTS
18//! ([`Node`]) with sources. Every number the design derives is therefore labeled modeled: a design is not a part.
19
20use serde::Serialize;
21
22use super::decode::{price, Chip, DecodeRefusal, DecodeStep, Link, Machine, Tier, TierKind};
23use super::rdu_chip::{Fact, Provenance};
24use crate::codegen_sva::fabric_target::Precision;
25
26/// A process node, as an SRAM decode chip sees it.
27#[derive(Clone, Debug, PartialEq, Serialize)]
28pub struct Node {
29    pub name: &'static str,
30    /// SRAM bits per mm² of die area given to SRAM.
31    pub sram_bits_per_mm2: Fact<f64>,
32    /// Bytes the SRAM reads per cycle per byte it holds: the macros' read width over their size.
33    pub sram_read_per_cycle_per_byte: Fact<f64>,
34    pub clock_ghz: Fact<f64>,
35    pub sources: &'static [&'static str],
36}
37
38const SKY130_SRAM_LEF: &str =
39    "open_pdks sky130A libs.ref/sky130_sram_macros/lef/sky130_sram_2kbyte_1rw1r_32x512_8.lef";
40const SKY130_SYSTOLIC_SIGNOFF: &str = "benchmarks/silicon/systolic_pipelined_signoff.json";
41const TSMC_N5_SRAM: &str = "tomshardware.com/news/no-sram-scaling-implies-on-more-expensive-cpus-and-gpus";
42const GROQ_SPEC_SHEET: &str = "groq.com/product-spec-sheet-groqchip-processor";
43
44impl Node {
45    /// SkyWater 130 nm, as our own signoff records it.
46    pub fn sky130() -> Node {
47        let macro_mm2 = 683.1 * 416.54 / 1e6;
48        Node {
49            name: "sky130",
50            sram_bits_per_mm2: Fact::derived(
51                2048.0 * 8.0 / macro_mm2,
52                "open_pdks sky130A libs.ref/sky130_sram_macros/lef/sky130_sram_2kbyte_1rw1r_32x512_8.lef: \"SIZE 683.1 BY \
53                 416.54\" for 2 KiB (\"2kbyte\"), the largest sky130 SRAM macro; 16,384 bits over 0.28454 mm², periphery \
54                 included",
55            ),
56            sram_read_per_cycle_per_byte: Fact::derived(
57                4.0 / 2048.0,
58                "open_pdks sky130A libs.ref/sky130_sram_macros/lef/sky130_sram_2kbyte_1rw1r_32x512_8.lef: \"1rw1r_32x512\", a \
59                 32-bit read port on 512 words — four bytes per cycle from each 2 KiB",
60            ),
61            clock_ghz: Fact::measured(
62                1.0 / 19.5,
63                "benchmarks/silicon/systolic_pipelined_signoff.json: systolic_pipe16_realbist \"clock_period_ns\": 19.5, \
64                 the largest array our OpenLane sky130 signoff has closed",
65            ),
66            sources: &[SKY130_SRAM_LEF, SKY130_SYSTOLIC_SIGNOFF],
67        }
68    }
69
70    /// TSMC N5. Its SRAM density is published; its read width and clock for an SRAM dataflow die are inherited
71    /// from the one such die whose figures are published, the 14 nm GroqChip — conservative at 5 nm.
72    pub fn n5() -> Node {
73        Node {
74            name: "n5",
75            sram_bits_per_mm2: Fact::published(
76                31.8 * 1024.0 * 1024.0,
77                "tomshardware.com/news/no-sram-scaling-implies-on-more-expensive-cpus-and-gpus (reporting WikiChip on TSMC's \
78                 IEDM 2022 paper): N5's \"0.021 µm^² SRAM bitcell (which roughly translates to 31.8 Mib/mm^²)\"",
79            ),
80            sram_read_per_cycle_per_byte: Fact::modeled(
81                80e12 / (230e6 * 0.9e9),
82                "not published for an N5 SRAM dataflow die; inherited from groq.com/product-spec-sheet-groqchip-processor: \
83                 \"230 MB SRAM per chip\", \"Up to 80 TB/s on-die memory bandwidth\", \"@900 MHz\" — 80e12 / (230e6 × 0.9e9) \
84                 bytes read per cycle per byte held",
85            ),
86            clock_ghz: Fact::modeled(
87                0.9,
88                "not published for an N5 SRAM dataflow die; inherited from groq.com/product-spec-sheet-groqchip-processor: \
89                 \"(INT8, FP16 @900 MHz)\" at 14nm",
90            ),
91            sources: &[TSMC_N5_SRAM, GROQ_SPEC_SHEET],
92        }
93    }
94
95    pub fn facts(&self) -> Vec<(String, Provenance, &'static str)> {
96        vec![
97            ("sram_bits_per_mm2".into(), self.sram_bits_per_mm2.provenance, self.sram_bits_per_mm2.cite),
98            (
99                "sram_read_per_cycle_per_byte".into(),
100                self.sram_read_per_cycle_per_byte.provenance,
101                self.sram_read_per_cycle_per_byte.cite,
102            ),
103            ("clock_ghz".into(), self.clock_ghz.provenance, self.clock_ghz.cite),
104        ]
105    }
106}
107
108/// How a dependent stage's vector reaches every tile and its partial sums come back.
109#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
110pub enum OnChipNetwork {
111    /// A 2-D mesh: out across its diameter and back.
112    Mesh,
113    /// A reduction / broadcast tree: out and back in `⌈log₂ tiles⌉` levels.
114    Tree,
115}
116
117/// A decode chip design point: the choices, over a node's facts.
118#[derive(Clone, Debug, PartialEq, Serialize)]
119pub struct DecodeChipDesign {
120    pub node: Node,
121    pub die_mm2: f64,
122    /// The share of the die that is SRAM array.
123    pub sram_fraction: f64,
124    /// SRAM per tile, bytes.
125    pub tile_bytes: f64,
126    pub network: OnChipNetwork,
127    /// Cycles one on-chip network hop takes: one is a registered link.
128    pub hop_cycles: f64,
129    /// UCIe modules each chip's collectives stripe across.
130    pub link_modules: u32,
131}
132
133/// UCIe's published die-to-die figures (UCIe Consortium, Hot Chips 2023 tutorial "UCIe overview and usage models").
134const UCIE_TUTORIAL: &str = "hc2023.hotchips.org/assets/program/tutorials/ucie/UCIe overview and usage models.pdf";
135
136/// The die-to-die link a decode system's chips connect with, at a hop latency the requirement solves for: `modules`
137/// UCIe standard-package modules striped together.
138pub fn ucie_link(hop_latency_s: f64, modules: u32) -> Link {
139    Link {
140        bandwidth: Some(Fact::derived(
141            64e9 * modules as f64,
142            "hc2023.hotchips.org/assets/program/tutorials/ucie/UCIe overview and usage models.pdf: \"B/W per module/ dir: \
143             64 GB/s Std, 256 GB/s Adv\"; the standard-package module rate times the modules a chip stripes across",
144        )),
145        hop_latency: Some(Fact::modeled(
146            hop_latency_s,
147            "not published for a chip-to-chip collective hop; the latency under test (UCIe's own \"Latency (Tx + Rx) < 2ns\" \
148             is a die-to-die link inside one package)",
149        )),
150    }
151}
152
153impl DecodeChipDesign {
154    /// A dataflow die at `node`: half its area SRAM, 256 KiB tiles on a registered reduction tree, sixteen UCIe
155    /// modules to its neighbours.
156    pub fn at(node: Node, die_mm2: f64) -> DecodeChipDesign {
157        DecodeChipDesign {
158            node,
159            die_mm2,
160            sram_fraction: 0.5,
161            tile_bytes: 256.0 * 1024.0,
162            network: OnChipNetwork::Tree,
163            hop_cycles: 1.0,
164            link_modules: 16,
165        }
166    }
167
168    /// The design's chip-to-chip link at `hop_latency_s`.
169    pub fn link(&self, hop_latency_s: f64) -> Link {
170        ucie_link(hop_latency_s, self.link_modules)
171    }
172
173    pub fn sram_bytes(&self) -> f64 {
174        self.die_mm2 * self.sram_fraction * self.node.sram_bits_per_mm2.value / 8.0
175    }
176
177    pub fn tiles(&self) -> f64 {
178        (self.sram_bytes() / self.tile_bytes).max(1.0)
179    }
180
181    /// Hops one dependent stage makes on chip: its vector out to every tile and its partial sums back.
182    pub fn stage_hops(&self) -> f64 {
183        let t = self.tiles();
184        match self.network {
185            OnChipNetwork::Mesh => 2.0 * 2.0 * (t.sqrt().ceil() - 1.0),
186            OnChipNetwork::Tree => 2.0 * t.log2().ceil(),
187        }
188    }
189
190    /// Seconds one dependent stage spends on the network.
191    pub fn stage_latency_s(&self) -> f64 {
192        self.stage_hops() * self.hop_cycles / (self.node.clock_ghz.value * 1e9)
193    }
194
195    /// The chip, with a chip-to-chip link and a per-stage overhead of `stage_s` (the design's own, or a budget
196    /// under test).
197    pub fn chip_with(&self, stage_s: f64, link: Link) -> Chip {
198        let bytes = self.sram_bytes();
199        let bandwidth = bytes * self.node.sram_read_per_cycle_per_byte.value * self.node.clock_ghz.value * 1e9;
200        let modeled = |value: f64, cite: &'static str| Fact::modeled(value, cite);
201        Chip {
202            name: "logos-decode",
203            tiers: vec![Tier {
204                kind: TierKind::Sram,
205                capacity: modeled(bytes, "not published: a LOGOS design point — die × SRAM share × the node's SRAM density"),
206                bandwidth: modeled(bandwidth, "not published: a LOGOS design point — SRAM held × the node's read per cycle per byte × its clock"),
207            }],
208            matrix: [Precision::Int8, Precision::Fp8]
209                .into_iter()
210                .map(|p| {
211                    // One multiply-add per weight byte read: the arithmetic a batch-1 decode asks of each byte, so
212                    // compute is sized to the SRAM and never binds.
213                    (p, modeled(2.0 * bandwidth, "not published: a LOGOS design point — one multiply-add per SRAM byte read per cycle, inherited from the decode's own arithmetic intensity"))
214                })
215                .collect(),
216            stage_overhead: Some(modeled(stage_s, "not published: a LOGOS design point — the on-chip network's hops per dependent stage over the node's clock, or a budget under test")),
217            link: Some(link),
218            power: None,
219            sources: &[UCIE_TUTORIAL],
220        }
221    }
222}
223
224/// What a LOGOS system must achieve to beat a GPU deployment's floor by a speedup on one checkpoint.
225#[derive(Clone, Debug, PartialEq, Serialize)]
226pub struct Requirement {
227    pub node: &'static str,
228    pub die_mm2: f64,
229    pub network: OnChipNetwork,
230    /// The fewest chips whose SRAM holds the deployment.
231    pub chips: u32,
232    pub sram_per_chip_bytes: f64,
233    /// Tokens per second the system must reach: the GPU floor times the speedup.
234    pub target_tok_s: f64,
235    /// The system's own floor at those chips: its memory roofline.
236    pub floor_tok_s: f64,
237    /// Dependent stages per token, and those that reduce across chips.
238    pub stages: usize,
239    pub collectives: usize,
240    /// The most one dependent stage may take on chip with chip-to-chip hops free, or `None` if even free
241    /// stages miss the target.
242    pub stage_budget_s: Option<f64>,
243    /// The design's own on-chip stage latency.
244    pub design_stage_s: f64,
245    /// The most one chip-to-chip hop may take with the design's own stage latency, or `None` if the design's
246    /// stage latency alone misses the target.
247    pub hop_budget_s: Option<f64>,
248    /// Predicted tokens per second at the design's stage latency, by chip-to-chip hop latency.
249    pub by_hop_latency: Vec<(f64, f64)>,
250}
251
252/// The largest `x` in `[0, hi]` with `ok(x)`, for `ok` true at 0 and false past some point; `None` if false at 0.
253fn largest(ok: impl Fn(f64) -> bool, hi: f64) -> Option<f64> {
254    if !ok(0.0) {
255        return None;
256    }
257    if ok(hi) {
258        return Some(hi);
259    }
260    let (mut lo, mut up) = (0.0, hi);
261    for _ in 0..200 {
262        let mid = 0.5 * (lo + up);
263        if ok(mid) {
264            lo = mid;
265        } else {
266            up = mid;
267        }
268    }
269    Some(lo)
270}
271
272/// The fewest chips of `chip` that hold `step`.
273pub fn fewest_chips(step: &DecodeStep, chip: &Chip, precision: Precision) -> Result<u32, DecodeRefusal> {
274    let per_chip: f64 = chip.tiers.iter().map(|t| t.capacity.value).sum();
275    let mut n = (step.held_bytes() / per_chip).ceil().max(1.0) as u32;
276    loop {
277        match price(step, &Machine::of(chip.clone(), n), precision) {
278            Ok(_) => return Ok(n),
279            Err(DecodeRefusal::NotResident { .. }) if n < 1 << 20 => n += 1,
280            Err(e) => return Err(e),
281        }
282    }
283}
284
285/// What `design` must achieve to decode `step` at `target_tok_s`.
286pub fn requirement(step: &DecodeStep, design: &DecodeChipDesign, target_tok_s: f64, precision: Precision) -> Result<Requirement, DecodeRefusal> {
287    let own = design.stage_latency_s();
288    let chips = fewest_chips(step, &design.chip_with(own, design.link(0.0)), precision)?;
289    let time = |stage_s: f64, hop_s: f64| -> f64 {
290        let cost = price(step, &Machine::of(design.chip_with(stage_s, design.link(hop_s)), chips), precision)
291            .expect("the fewest chips hold it");
292        cost.time_s.expect("every term of a design point is declared")
293    };
294    let target_s = 1.0 / target_tok_s;
295    let base = price(step, &Machine::of(design.chip_with(own, design.link(0.0)), chips), precision)?;
296    let stage_budget_s = largest(|s| time(s, 0.0) <= target_s, 1e-3);
297    let hop_budget_s = largest(|h| time(own, h) <= target_s, 1e-3);
298    let by_hop_latency = [2e-9, 50e-9, 100e-9, 250e-9, 500e-9, 1e-6]
299        .into_iter()
300        .map(|h| (h, 1.0 / time(own, h)))
301        .collect();
302    Ok(Requirement {
303        node: design.node.name,
304        die_mm2: design.die_mm2,
305        network: design.network,
306        chips,
307        sram_per_chip_bytes: design.sram_bytes(),
308        target_tok_s,
309        floor_tok_s: base.floor_tokens_per_s(),
310        stages: step.stages.len(),
311        collectives: base.collectives,
312        stage_budget_s,
313        design_stage_s: own,
314        hop_budget_s,
315        by_hop_latency,
316    })
317}
318
319/// The per-stage latency a published per-user rate implies: everything a token took beyond its memory floor,
320/// spread over its dependent stages. A rate this machine reached is a latency it has shown.
321pub fn demonstrated_stage_latency(step: &DecodeStep, machine: &Machine, published_tok_s: f64, precision: Precision) -> Result<f64, DecodeRefusal> {
322    let cost = price(step, machine, precision)?;
323    Ok(((1.0 / published_tok_s) - cost.floor_s).max(0.0) / step.stages.len() as f64)
324}
325
326// ── The report ──────────────────────────────────────────────────────────────────────────────────────
327
328/// A GPU deployment's floor on one checkpoint.
329#[derive(Clone, Debug, PartialEq, Serialize)]
330pub struct GpuFloor {
331    pub machine: &'static str,
332    pub chips: u32,
333    pub floor_tok_s: f64,
334    pub binds: super::decode::Bind,
335}
336
337/// A vendor's published per-user rate, and the per-stage latency it implies.
338#[derive(Clone, Debug, PartialEq, Serialize)]
339pub struct Demonstrated {
340    pub label: String,
341    pub machine: &'static str,
342    pub chips: u32,
343    pub published_tok_s: f64,
344    pub stage_latency_s: f64,
345}
346
347/// One requirement: a GPU baseline, the speedup over its floor, the design asked to reach it, and the answer.
348#[derive(Clone, Debug, PartialEq, Serialize)]
349pub struct RequirementRow {
350    pub baseline: String,
351    pub baseline_floor_tok_s: f64,
352    pub speedup: f64,
353    pub requirement: Requirement,
354}
355
356/// Everything the report says about one checkpoint.
357#[derive(Clone, Debug, PartialEq, Serialize)]
358pub struct ModelReport {
359    pub model: String,
360    pub weight_bytes_per_token: f64,
361    pub held_bytes: f64,
362    pub stages: usize,
363    pub gpu_floors: Vec<GpuFloor>,
364    pub demonstrated: Vec<Demonstrated>,
365    pub requirements: Vec<RequirementRow>,
366}
367
368/// The decode-chip report: what a LOGOS system must achieve, checkpoint by checkpoint.
369#[derive(Clone, Debug, PartialEq, Serialize)]
370pub struct DecodeChipReport {
371    pub what: &'static str,
372    pub deployment: super::decode::Deployment,
373    pub speedup: f64,
374    pub nodes: Vec<Node>,
375    pub models: Vec<ModelReport>,
376}
377
378/// The checkpoints the report prices, by header ledger name.
379pub const REPORT_MODELS: &[&str] = &[
380    "llama-3.1-8b-instruct-q8_0",
381    "llama-3.1-70b-instruct-q4_k_m",
382    "qwen3.8-27b-q8_0",
383];
384
385/// The report over the header and published ledgers under `decode_dir` (`benchmarks/silicon/decode`).
386pub fn report(decode_dir: &std::path::Path) -> Result<DecodeChipReport, String> {
387    use super::decode::{Deployment, GgufHeader, WeightBytes};
388    let precision = Precision::Fp8;
389    let speedup = 5.0;
390    // Both sides at the same numerics: fp8 weights, an f16 KV cache at a 4096-token context.
391    let deployment = Deployment { weights: WeightBytes::PerWeight(1.0), ..Deployment::as_stored(4096) };
392    let published: serde_json::Value = serde_json::from_str(
393        &std::fs::read_to_string(decode_dir.join("published.json")).map_err(|e| e.to_string())?,
394    )
395    .map_err(|e| e.to_string())?;
396    let designs = [
397        DecodeChipDesign::at(Node::n5(), 800.0),
398        DecodeChipDesign { network: OnChipNetwork::Mesh, ..DecodeChipDesign::at(Node::n5(), 800.0) },
399        DecodeChipDesign::at(Node::sky130(), 800.0),
400    ];
401    let mut models = Vec::new();
402    for &name in REPORT_MODELS {
403        let header = GgufHeader::read_ledger(&decode_dir.join(format!("headers/{name}.json")))?;
404        let step = DecodeStep::of(&header, &deployment).map_err(|e| format!("{name}: {e:?}"))?;
405        let mut gpu_floors = Vec::new();
406        for chip in [Chip::h100_sxm(), Chip::b200()] {
407            for chips in [1u32, 8] {
408                if let Ok(cost) = price(&step, &Machine::of(chip.clone(), chips), precision) {
409                    gpu_floors.push(GpuFloor { machine: chip.name, chips, floor_tok_s: cost.floor_tokens_per_s(), binds: cost.floor_binds });
410                }
411            }
412        }
413        let mut demonstrated = Vec::new();
414        for row in published["rows"].as_array().ok_or("published rows")? {
415            if row["header"].as_str() != Some(name) {
416                continue;
417            }
418            let chip = Chip::by_name(row["machine"].as_str().ok_or("machine")?).ok_or("a catalog chip")?;
419            let mut d = Deployment::as_stored(row["ctx"].as_u64().ok_or("ctx")?);
420            if let Some(w) = row["bytes_per_weight"].as_f64() {
421                d.weights = WeightBytes::PerWeight(w);
422            }
423            let theirs = DecodeStep::of(&header, &d).map_err(|e| format!("{e:?}"))?;
424            let chips = fewest_chips(&theirs, &chip, precision).map_err(|e| format!("{e:?}"))?;
425            let tok_s = row["tok_s"].as_f64().ok_or("tok_s")?;
426            demonstrated.push(Demonstrated {
427                label: row["label"].as_str().unwrap_or_default().to_string(),
428                machine: chip.name,
429                chips,
430                published_tok_s: tok_s,
431                stage_latency_s: demonstrated_stage_latency(&theirs, &Machine::of(chip, chips), tok_s, precision).map_err(|e| format!("{e:?}"))?,
432            });
433        }
434        let mut requirements = Vec::new();
435        for floor in &gpu_floors {
436            for design in &designs {
437                let r = requirement(&step, design, floor.floor_tok_s * speedup, precision).map_err(|e| format!("{name}: {e:?}"))?;
438                requirements.push(RequirementRow {
439                    baseline: format!("{} x {}", floor.chips, floor.machine),
440                    baseline_floor_tok_s: floor.floor_tok_s,
441                    speedup,
442                    requirement: r,
443                });
444            }
445        }
446        models.push(ModelReport {
447            model: name.to_string(),
448            weight_bytes_per_token: step.weight_bytes(),
449            held_bytes: step.held_bytes(),
450            stages: step.stages.len(),
451            gpu_floors,
452            demonstrated,
453            requirements,
454        });
455    }
456    Ok(DecodeChipReport {
457        what: "What a system of SRAM-resident LOGOS decode chips must achieve to beat a GPU deployment's FLOOR (its memory \
458               roofline with every overhead at zero) by the speedup, per user at batch one. Each requirement is a pair of \
459               latency budgets: the most one dependent stage may take on chip with chip-to-chip hops free, and the most one \
460               chip-to-chip hop may take at the design's own on-chip stage latency. `demonstrated` is the per-stage latency a \
461               vendor's published per-user rate implies, the yardstick for a budget. Regenerate with `cargo run -p \
462               logicaffeine-compile --example decode_chip_report`; spatial_decode_chip_lock holds the file to the model.",
463        deployment,
464        speedup,
465        nodes: vec![Node::n5(), Node::sky130()],
466        models,
467    })
468}