1use 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#[derive(Clone, Debug, PartialEq, Serialize)]
28pub struct Node {
29 pub name: &'static str,
30 pub sram_bits_per_mm2: Fact<f64>,
32 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
110pub enum OnChipNetwork {
111 Mesh,
113 Tree,
115}
116
117#[derive(Clone, Debug, PartialEq, Serialize)]
119pub struct DecodeChipDesign {
120 pub node: Node,
121 pub die_mm2: f64,
122 pub sram_fraction: f64,
124 pub tile_bytes: f64,
126 pub network: OnChipNetwork,
127 pub hop_cycles: f64,
129 pub link_modules: u32,
131}
132
133const UCIE_TUTORIAL: &str = "hc2023.hotchips.org/assets/program/tutorials/ucie/UCIe overview and usage models.pdf";
135
136pub 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 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 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 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 pub fn stage_latency_s(&self) -> f64 {
192 self.stage_hops() * self.hop_cycles / (self.node.clock_ghz.value * 1e9)
193 }
194
195 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 (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#[derive(Clone, Debug, PartialEq, Serialize)]
226pub struct Requirement {
227 pub node: &'static str,
228 pub die_mm2: f64,
229 pub network: OnChipNetwork,
230 pub chips: u32,
232 pub sram_per_chip_bytes: f64,
233 pub target_tok_s: f64,
235 pub floor_tok_s: f64,
237 pub stages: usize,
239 pub collectives: usize,
240 pub stage_budget_s: Option<f64>,
243 pub design_stage_s: f64,
245 pub hop_budget_s: Option<f64>,
248 pub by_hop_latency: Vec<(f64, f64)>,
250}
251
252fn 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
272pub 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
285pub 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
319pub 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#[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#[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#[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#[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#[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
378pub 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
385pub 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 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}