Skip to main content

logicaffeine_compile/spatial/
roofs.rs

1//! `Roofs` — the rates a chip sustains, each one cited: the roofline a program's work is held against.
2//!
3//! A [`SpatialTarget`](super::target::SpatialTarget) says what a chip HOLDS: sites, local memory, the
4//! accumulator. This says how fast it MOVES and COMPUTES: bytes per second off the chip, bytes per cycle
5//! across one network link, operations per cycle of a site's matrix engine. A program's work over a rate is
6//! a lower bound on its time; the largest of those bounds is the roof that binds it; and a measured or
7//! predicted time below the bound is a wrong byte count or a wrong roof, never a fast program (Williams,
8//! Waterman & Patterson, *Roofline: an insightful visual performance model for multicore architectures*,
9//! CACM 52(4), 2009).
10//!
11//! Every rate is a [`Fact`] in the unit its source states it in. A vendor's GB/s stays per second, a link's
12//! width stays per cycle, and [`Roofs::per_cycle`] / [`Roofs::per_second`] convert through the clock with
13//! the weaker of the two provenances, so a published bandwidth never borrows a modeled clock's authority. A
14//! rate the record does not give is absent rather than guessed: [`Roofs::mem`] and [`Roofs::compute`]
15//! answer `None`, and nothing downstream may price a resource without a roof.
16//!
17//! An operation is the vendors' unit: a multiply-add counts two.
18
19use serde::Serialize;
20
21use super::rdu_chip::{Fact, Provenance};
22use crate::codegen_sva::fabric_target::Precision;
23
24/// Where bytes move.
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
26pub enum MemLevel {
27    /// The off-chip memory every site shares: DRAM, GDDR or HBM.
28    Dram,
29    /// One link of the on-chip network between sites.
30    Noc,
31    /// A site's own memory: L1, shared memory, a scratchpad.
32    Local,
33}
34
35/// The access mix a bandwidth holds for — the four STREAM kernels (McCalpin, *Memory Bandwidth and Machine
36/// Balance in Current High Performance Computers*, IEEE TCCA Newsletter, 1995) — or `Any`: a peak that
37/// bounds every mix, the way a bus's data rate bounds whatever crosses it.
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
39pub enum StreamMix {
40    Any,
41    Read,
42    Write,
43    Copy,
44    Triad,
45}
46
47/// What one rate is counted over.
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
49pub enum Scope {
50    /// The whole chip, shared by every site.
51    Chip,
52    /// Each site, independently of the others.
53    Site,
54    /// Each link of a network, independently of the others.
55    Link,
56}
57
58/// The engine an operation rate belongs to.
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
60pub enum Engine {
61    /// The tile / tensor-core / systolic matrix engine.
62    Matrix,
63    /// The SIMD or elementwise datapath.
64    Vector,
65}
66
67/// What a memory rate counts. A bus moves bytes whatever they hold; an element-serial lane carries one
68/// element per cycle whatever its width, so its traffic is counted in elements.
69#[derive(
70    Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, serde::Deserialize,
71)]
72pub enum Transfer {
73    Bytes,
74    Elements,
75}
76
77/// A rate in the unit its source states it in.
78#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
79pub enum Rate {
80    PerSecond(f64),
81    PerCycle(f64),
82}
83
84/// A bandwidth at one level of the hierarchy, for one access mix, over one scope, in the unit the transfer
85/// counts.
86#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
87pub struct MemRoof {
88    pub level: MemLevel,
89    pub mix: StreamMix,
90    pub scope: Scope,
91    pub unit: Transfer,
92    pub rate: Fact<Rate>,
93}
94
95/// A throughput: operations of one engine at one precision, over one scope. For an engine whose math
96/// fidelity is a number of passes over the operands, this is the rate of ONE pass: the work model already
97/// counts each pass as work ([`super::work::fidelity_passes`]), so a roof that divided by them again would
98/// charge the fidelity twice.
99#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
100pub struct ComputeRoof {
101    pub engine: Engine,
102    pub precision: Precision,
103    pub scope: Scope,
104    pub ops: Fact<Rate>,
105}
106
107/// A rate converted to the unit asked for, with the provenance of the weaker of the rate and the clock.
108#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
109pub struct Converted {
110    pub value: f64,
111    pub provenance: Provenance,
112}
113
114/// A chip's roofs.
115#[derive(Clone, Debug, PartialEq, Serialize)]
116pub struct Roofs {
117    /// The clock the per-cycle rates run at, in GHz.
118    pub clock_ghz: Fact<f64>,
119    pub mem: Vec<MemRoof>,
120    pub compute: Vec<ComputeRoof>,
121    /// The documents every published or derived roof is read from; each such cite names one of them.
122    pub sources: &'static [&'static str],
123}
124
125impl Roofs {
126    /// The bandwidth of `level` for `mix`: a roof measured for that very mix when there is one, else the peak
127    /// that bounds every mix.
128    pub fn mem(&self, level: MemLevel, mix: StreamMix) -> Option<&MemRoof> {
129        let at = |m: StreamMix| self.mem.iter().find(|r| r.level == level && r.mix == m);
130        at(mix).or_else(|| at(StreamMix::Any))
131    }
132
133    /// The throughput of `engine` at `precision`.
134    pub fn compute(&self, engine: Engine, precision: Precision) -> Option<&ComputeRoof> {
135        self.compute
136            .iter()
137            .find(|r| r.engine == engine && r.precision == precision)
138    }
139
140    /// `rate` per cycle of this chip's clock.
141    pub fn per_cycle(&self, rate: &Fact<Rate>) -> Converted {
142        match rate.value {
143            Rate::PerCycle(v) => Converted {
144                value: v,
145                provenance: rate.provenance,
146            },
147            Rate::PerSecond(v) => Converted {
148                value: v / (self.clock_ghz.value * 1e9),
149                provenance: rate.provenance.max(self.clock_ghz.provenance),
150            },
151        }
152    }
153
154    /// `rate` per second at this chip's clock.
155    pub fn per_second(&self, rate: &Fact<Rate>) -> Converted {
156        match rate.value {
157            Rate::PerSecond(v) => Converted {
158                value: v,
159                provenance: rate.provenance,
160            },
161            Rate::PerCycle(v) => Converted {
162                value: v * self.clock_ghz.value * 1e9,
163                provenance: rate.provenance.max(self.clock_ghz.provenance),
164            },
165        }
166    }
167
168    /// Every fact the roofs carry, the clock first, each with the path a lock names it by.
169    pub fn facts(&self) -> Vec<(String, Provenance, &'static str)> {
170        let mut out = vec![(
171            "clock_ghz".to_string(),
172            self.clock_ghz.provenance,
173            self.clock_ghz.cite,
174        )];
175        for r in &self.mem {
176            out.push((
177                format!("mem.{:?}.{:?}.{:?}", r.level, r.mix, r.scope),
178                r.rate.provenance,
179                r.rate.cite,
180            ));
181        }
182        for r in &self.compute {
183            out.push((
184                format!("compute.{:?}.{:?}.{:?}", r.engine, r.precision, r.scope),
185                r.ops.provenance,
186                r.ops.cite,
187            ));
188        }
189        out
190    }
191}
192
193// ── The presets ──────────────────────────────────────────────────────────────────────────────────────────
194
195const TT_WORMHOLE_SPECS: &str =
196    "docs.tenstorrent.com/docs-test/core/latest/aibs/wormhole/specifications.html";
197const TT_BLACKHOLE_SPECS: &str = "docs.tenstorrent.com/aibs/blackhole/index.html";
198const TT_GEMM_FLOPS: &str =
199    "github.com/tenstorrent/tt-metal/blob/main/tech_reports/GEMM_FLOPS/GEMM_FLOPS.md";
200const TT_WORMHOLE_NOC: &str =
201    "github.com/tenstorrent/tt-isa-documentation/blob/main/WormholeB0/NoC/README.md";
202const TT_BLACKHOLE_NOC: &str =
203    "github.com/tenstorrent/tt-isa-documentation/blob/main/BlackholeA0/NoC/README.md";
204const NV_RTX3060_SPECS: &str = "nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3060-3060ti";
205const NV_GA102_WHITEPAPER: &str =
206    "nvidia.com/content/PDF/nvidia-ampere-ga-102-gpu-architecture-whitepaper-v2.pdf";
207pub(crate) const NV_H100_PAGE: &str = "nvidia.com/en-us/data-center/h100";
208pub(crate) const NV_H100_NVL_BRIEF: &str =
209    "nvidia.com/content/dam/en-zz/Solutions/Data-Center/h100/PB-11773-001_v01.pdf";
210pub(crate) const NV_A100_PAGE: &str = "nvidia.com/en-us/data-center/a100";
211pub(crate) const NV_DGX_B200_PAGE: &str = "nvidia.com/en-us/data-center/dgx-b200";
212pub(crate) const NV_HGX_PAGE: &str = "nvidia.com/en-us/data-center/hgx";
213const FABRIC_TWIN: &str = "spatial::lower::fabric::fabric_cycle_model, cycle-exact against the RTL by spatial_fabric_lowering_lock";
214const SKY130_SYSTOLIC_SIGNOFF: &str = "benchmarks/silicon/systolic_pipelined_signoff.json";
215const DIGILENT_NEXYS_CLOCK: &str = "digilent.com/reference/programmable-logic/nexys-a7/reference-manual and \
216    digilent.com/reference/programmable-logic/nexys-video/reference-manual: a 100 MHz crystal oscillator on the FPGA's \
217    clock-capable input";
218
219/// The Tensix matrix engine: one 8x16 by 16x16 product per cycle per pass, the same engine on both parts.
220const TENSIX_MATRIX: &str = "github.com/tenstorrent/tt-metal/blob/main/tech_reports/GEMM_FLOPS/GEMM_FLOPS.md: \
221    \"The matrix engine performs 8x16 x 16x16 = 8x16 in a single cycle. This is 2*8*16*16 = 4096 multiply-adds in a \
222    single cycle\" (4096 operations: 2048 multiply-adds); \"LoFi ~4 TFLOPS, HiFi2 ~2 TFLOPS, HiFi4 ~1 TFLOPS\" per engine \
223    at 1 GHz, so one pass per cycle";
224
225/// Matrix roofs from a vendor table that states rates "with sparsity": the dense rate is half of each.
226fn dense_from_sparse(rows: &[(Precision, f64, &'static str)]) -> Vec<ComputeRoof> {
227    rows.iter()
228        .map(|&(precision, sparse, cite)| ComputeRoof {
229            engine: Engine::Matrix,
230            precision,
231            scope: Scope::Chip,
232            ops: Fact::derived(Rate::PerSecond(sparse / 2.0), cite),
233        })
234        .collect()
235}
236
237fn tensix_matrix(precisions: &[Precision]) -> Vec<ComputeRoof> {
238    precisions
239        .iter()
240        .map(|&precision| ComputeRoof {
241            engine: Engine::Matrix,
242            precision,
243            scope: Scope::Site,
244            ops: Fact::published(Rate::PerCycle(4096.0), TENSIX_MATRIX),
245        })
246        .collect()
247}
248
249impl Roofs {
250    /// Tenstorrent Wormhole n150.
251    pub fn wormhole() -> Roofs {
252        Roofs {
253            clock_ghz: Fact::published(1.0, "docs.tenstorrent.com/docs-test/core/latest/aibs/wormhole/specifications.html: n150s \"AI Clock: 1 GHz\""),
254            mem: vec![
255                MemRoof {
256                    level: MemLevel::Dram,
257                    mix: StreamMix::Any,
258                    scope: Scope::Chip,
259                    unit: Transfer::Bytes,
260                    rate: Fact::published(
261                        Rate::PerSecond(288e9),
262                        "docs.tenstorrent.com/docs-test/core/latest/aibs/wormhole/specifications.html: n150s \"Memory: 12GB GDDR6\", \
263                         \"Memory Speed: 12 GT/sec\", \"Memory Bandwidth: 288 GB/sec\"",
264                    ),
265                },
266                MemRoof {
267                    level: MemLevel::Noc,
268                    mix: StreamMix::Any,
269                    scope: Scope::Link,
270                    unit: Transfer::Bytes,
271                    rate: Fact::published(
272                        Rate::PerCycle(32.0),
273                        "github.com/tenstorrent/tt-isa-documentation/blob/main/WormholeB0/NoC/README.md: \"Each flit consists of exactly \
274                         256 bits (32 bytes)\", \"One flit (256 bits) per cycle\"",
275                    ),
276                },
277            ],
278            compute: tensix_matrix(&[Precision::Bf16, Precision::Fp16]),
279            sources: &[TT_WORMHOLE_SPECS, TT_GEMM_FLOPS, TT_WORMHOLE_NOC],
280        }
281    }
282
283    /// Tenstorrent Blackhole p150.
284    pub fn blackhole() -> Roofs {
285        Roofs {
286            clock_ghz: Fact::published(1.35, "docs.tenstorrent.com/aibs/blackhole/index.html: p150a/p150b \"AI Clock: Up to 1.35 GHz\""),
287            mem: vec![
288                MemRoof {
289                    level: MemLevel::Dram,
290                    mix: StreamMix::Any,
291                    scope: Scope::Chip,
292                    unit: Transfer::Bytes,
293                    rate: Fact::published(
294                        Rate::PerSecond(512e9),
295                        "docs.tenstorrent.com/aibs/blackhole/index.html: p150a/p150b \"32 GB GDDR6\", \"Memory Speed: 16 GT/sec\", \
296                         \"Memory Bandwidth: 512 GB/sec\"",
297                    ),
298                },
299                MemRoof {
300                    level: MemLevel::Noc,
301                    mix: StreamMix::Any,
302                    scope: Scope::Link,
303                    unit: Transfer::Bytes,
304                    rate: Fact::published(
305                        Rate::PerCycle(64.0),
306                        "github.com/tenstorrent/tt-isa-documentation/blob/main/BlackholeA0/NoC/README.md: \"Each flit consists of exactly \
307                         512 bits (64 bytes)\", \"One flit (512 bits) per cycle per axis\"",
308                    ),
309                },
310            ],
311            compute: tensix_matrix(&[Precision::Bf16, Precision::Fp16]),
312            sources: &[TT_BLACKHOLE_SPECS, TT_GEMM_FLOPS, TT_BLACKHOLE_NOC],
313        }
314    }
315
316    /// The local RTX 3060. NVIDIA's table gives the SM count (through the CUDA cores) and the boost clock but no
317    /// tensor rate and no memory data rate, so the tensor rates are derived from the GA10x SM the Ampere
318    /// whitepaper tabulates, and the DRAM rate is modeled until the box measures it.
319    pub fn rtx3060() -> Roofs {
320        Roofs {
321            clock_ghz: Fact::published(1.78, "nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3060-3060ti: RTX 3060 \"Boost Clock: 1.78 GHz\""),
322            mem: vec![
323                MemRoof {
324                    level: MemLevel::Dram,
325                    mix: StreamMix::Any,
326                    scope: Scope::Chip,
327                    unit: Transfer::Bytes,
328                    rate: Fact::modeled(
329                        Rate::PerSecond(360e9),
330                        "not published by NVIDIA: its RTX 3060 table gives \"12 GB GDDR6\" on a \"192-bit\" interface and no data \
331                         rate; the board partners' 15 Gbps GDDR6 gives 192 x 15 / 8 = 360 GB/s",
332                    ),
333                },
334                MemRoof {
335                    level: MemLevel::Local,
336                    mix: StreamMix::Any,
337                    scope: Scope::Chip,
338                    unit: Transfer::Bytes,
339                    rate: Fact::derived(
340                        Rate::PerCycle(28.0 * 128.0),
341                        "nvidia.com/content/PDF/nvidia-ampere-ga-102-gpu-architecture-whitepaper-v2.pdf: GA10x shared memory \
342                         \"128 bytes/clock per SM\"; nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3060-3060ti: \"CUDA Cores: \
343                         3584\" at the whitepaper's \"CUDA Cores / SM 128\" = 28 SMs; 28 x 128 B per cycle",
344                    ),
345                },
346            ],
347            compute: [Precision::Bf16, Precision::Fp16]
348                .into_iter()
349                .map(|precision| ComputeRoof {
350                    engine: Engine::Matrix,
351                    precision,
352                    scope: Scope::Chip,
353                    ops: Fact::derived(
354                        Rate::PerCycle(28.0 * 512.0),
355                        "nvidia.com/content/PDF/nvidia-ampere-ga-102-gpu-architecture-whitepaper-v2.pdf: GA10x \"Total FP16 FMA \
356                         operations per SM Dense: 512\", 1024 operations per SM per cycle with FP16 accumulate and half that with FP32 \
357                         accumulate (RTX 3080: \"Peak FP16 Tensor TFLOPS with FP16 Accumulate 119\", \"with FP32 Accumulate 59.5\"); \
358                         nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3060-3060ti: \"CUDA Cores: 3584\" = 28 SMs at the \
359                         whitepaper's \"CUDA Cores / SM 128\"; 28 x 512 at FP32 accumulate",
360                    ),
361                })
362                .chain(std::iter::once(ComputeRoof {
363                    engine: Engine::Matrix,
364                    precision: Precision::Int8,
365                    scope: Scope::Chip,
366                    ops: Fact::derived(
367                        Rate::PerCycle(28.0 * 2048.0),
368                        "nvidia.com/content/PDF/nvidia-ampere-ga-102-gpu-architecture-whitepaper-v2.pdf: INT8 tensor at twice the \
369                         FP16-accumulate rate (RTX 3080: \"Peak INT8 Tensor TOPS 238\" against FP16 119, 68 SMs at 1710 MHz = 2048 \
370                         per SM per cycle); nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3060-3060ti: \"CUDA Cores: 3584\" = \
371                         28 SMs; 28 x 2048",
372                    ),
373                }))
374                .collect(),
375            sources: &[NV_RTX3060_SPECS, NV_GA102_WHITEPAPER],
376        }
377    }
378
379    /// NVIDIA H100 SXM. The product table gives memory bandwidth and tensor rates "With sparsity"; a dense rate
380    /// is half of it, which is what a dense matrix-vector product can use. The table gives no clock, and every
381    /// rate here is per second, so nothing is converted through the one recorded.
382    pub fn h100_sxm() -> Roofs {
383        Roofs {
384            clock_ghz: Fact::modeled(
385                1.98,
386                "not published on NVIDIA's H100 product page; inherited from the third-party spec databases' 1980 MHz \
387                 boost — every rate below is per second, so no rate is converted through it",
388            ),
389            mem: vec![MemRoof {
390                level: MemLevel::Dram,
391                mix: StreamMix::Any,
392                scope: Scope::Chip,
393                unit: Transfer::Bytes,
394                rate: Fact::published(
395                    Rate::PerSecond(3.35e12),
396                    "nvidia.com/en-us/data-center/h100: H100 SXM \"GPU Memory Bandwidth 3.35TB/s\"",
397                ),
398            }],
399            compute: dense_from_sparse(&[
400                (Precision::Int8, 3958e12, "nvidia.com/en-us/data-center/h100: H100 SXM \"INT8 Tensor Core* 3,958 TOPS\", \"* With sparsity\"; dense is half"),
401                (Precision::Fp8, 3958e12, "nvidia.com/en-us/data-center/h100: H100 SXM \"FP8 Tensor Core* 3,958 teraFLOPS\", \"* With sparsity\"; dense is half"),
402                (Precision::Bf16, 1979e12, "nvidia.com/en-us/data-center/h100: H100 SXM \"BFLOAT16 Tensor Core* 1,979 teraFLOPS\", \"* With sparsity\"; dense is half"),
403                (Precision::Fp16, 1979e12, "nvidia.com/en-us/data-center/h100: H100 SXM \"FP16 Tensor Core* 1,979 teraFLOPS\", \"* With sparsity\"; dense is half"),
404            ]),
405            sources: &[NV_H100_PAGE],
406        }
407    }
408
409    /// NVIDIA H100 NVL: the PCIe card the Qwen3.8-27B decode runs were measured on. Its product brief gives the
410    /// clock and the exact memory bandwidth; the tensor rates are the product table's, halved from sparse.
411    pub fn h100_nvl() -> Roofs {
412        Roofs {
413            clock_ghz: Fact::published(
414                1.785,
415                "nvidia.com/content/dam/en-zz/Solutions/Data-Center/h100/PB-11773-001_v01.pdf: Table 1 \"GPU clocks Base: \
416                 1,080 MHz Boost: 1,785 MHz\"",
417            ),
418            mem: vec![MemRoof {
419                level: MemLevel::Dram,
420                mix: StreamMix::Any,
421                scope: Scope::Chip,
422                unit: Transfer::Bytes,
423                rate: Fact::published(
424                    Rate::PerSecond(3938e9),
425                    "nvidia.com/content/dam/en-zz/Solutions/Data-Center/h100/PB-11773-001_v01.pdf: Table 2 \"Peak memory \
426                     bandwidth 3,938 GB/s\" (the product page rounds it to \"3.9TB/s\")",
427                ),
428            }],
429            compute: dense_from_sparse(&[
430                (Precision::Int8, 3341e12, "nvidia.com/en-us/data-center/h100: H100 NVL \"INT8 Tensor Core* 3,341 TOPS\", \"* With sparsity\"; dense is half"),
431                (Precision::Fp8, 3341e12, "nvidia.com/en-us/data-center/h100: H100 NVL \"FP8 Tensor Core* 3,341 teraFLOPS\", \"* With sparsity\"; dense is half"),
432                (Precision::Bf16, 1671e12, "nvidia.com/en-us/data-center/h100: H100 NVL \"BFLOAT16 Tensor Core* 1,671 teraFLOPS\", \"* With sparsity\"; dense is half"),
433                (Precision::Fp16, 1671e12, "nvidia.com/en-us/data-center/h100: H100 NVL \"FP16 Tensor Core* 1,671 teraFLOPS\", \"* With sparsity\"; dense is half"),
434            ]),
435            sources: &[NV_H100_PAGE, NV_H100_NVL_BRIEF],
436        }
437    }
438
439    /// NVIDIA A100 80GB SXM. Its product table states the dense rate first and the sparse rate after it.
440    pub fn a100_sxm4_80gb() -> Roofs {
441        Roofs {
442            clock_ghz: Fact::modeled(
443                1.41,
444                "not published on NVIDIA's A100 product page; inherited from the A100 whitepaper's 1410 MHz boost — every \
445                 rate below is per second, so no rate is converted through it",
446            ),
447            mem: vec![MemRoof {
448                level: MemLevel::Dram,
449                mix: StreamMix::Any,
450                scope: Scope::Chip,
451                unit: Transfer::Bytes,
452                rate: Fact::published(
453                    Rate::PerSecond(2039e9),
454                    "nvidia.com/en-us/data-center/a100: A100 80GB SXM \"GPU Memory Bandwidth 2,039 GB/s\"",
455                ),
456            }],
457            compute: [
458                (Precision::Int8, 624e12, "nvidia.com/en-us/data-center/a100: A100 80GB SXM \"INT8 Tensor Core 624 TOPS | 1248 TOPS*\", \"* With sparsity\""),
459                (Precision::Bf16, 312e12, "nvidia.com/en-us/data-center/a100: A100 80GB SXM \"BFLOAT16 Tensor Core 312 TFLOPS | 624 TFLOPS*\", \"* With sparsity\""),
460                (Precision::Fp16, 312e12, "nvidia.com/en-us/data-center/a100: A100 80GB SXM \"FP16 Tensor Core 312 TFLOPS | 624 TFLOPS*\", \"* With sparsity\""),
461            ]
462            .into_iter()
463            .map(|(precision, ops, cite)| ComputeRoof {
464                engine: Engine::Matrix,
465                precision,
466                scope: Scope::Chip,
467                ops: Fact::published(Rate::PerSecond(ops), cite),
468            })
469            .collect(),
470            sources: &[NV_A100_PAGE],
471        }
472    }
473
474    /// One GPU of the NVIDIA DGX/HGX B200. NVIDIA tabulates the 8-GPU system, so each rate is the system's over
475    /// eight, and each tensor rate the sparse figure halved per the table's own footnote.
476    pub fn b200() -> Roofs {
477        Roofs {
478            clock_ghz: Fact::modeled(
479                1.965,
480                "not published on NVIDIA's DGX B200 or HGX pages; inherited from third-party spec databases — every rate \
481                 below is per second, so no rate is converted through it",
482            ),
483            mem: vec![MemRoof {
484                level: MemLevel::Dram,
485                mix: StreamMix::Any,
486                scope: Scope::Chip,
487                unit: Transfer::Bytes,
488                rate: Fact::derived(
489                    Rate::PerSecond(64e12 / 8.0),
490                    "nvidia.com/en-us/data-center/dgx-b200: \"8x NVIDIA Blackwell GPUs\", \"1,440 GB total, 64 TB/s HBM3e \
491                     bandwidth\"; 64 TB/s over 8 GPUs",
492                ),
493            }],
494            compute: [
495                (Precision::Int8, 72e15, "nvidia.com/en-us/data-center/hgx: HGX B200 \"8x NVIDIA Blackwell SXM\", \"INT8 Tensor Core 72 POPS\", footnote \"Specification in Sparse. Dense is ½ sparse spec shown.\"; halved, over 8 GPUs"),
496                (Precision::Fp8, 72e15, "nvidia.com/en-us/data-center/hgx: HGX B200 \"8x NVIDIA Blackwell SXM\", \"FP8/FP6 Tensor Core 72 PFLOPS\", footnote \"Specification in Sparse. Dense is ½ sparse spec shown.\"; halved, over 8 GPUs"),
497                (Precision::Bf16, 36e15, "nvidia.com/en-us/data-center/hgx: HGX B200 \"8x NVIDIA Blackwell SXM\", \"FP16/BF16 Tensor Core 36 PFLOPS\", footnote \"Specification in Sparse. Dense is ½ sparse spec shown.\"; halved, over 8 GPUs"),
498                (Precision::Fp16, 36e15, "nvidia.com/en-us/data-center/hgx: HGX B200 \"8x NVIDIA Blackwell SXM\", \"FP16/BF16 Tensor Core 36 PFLOPS\", footnote \"Specification in Sparse. Dense is ½ sparse spec shown.\"; halved, over 8 GPUs"),
499            ]
500            .into_iter()
501            .map(|(precision, sparse_system, cite)| ComputeRoof {
502                engine: Engine::Matrix,
503                precision,
504                scope: Scope::Chip,
505                ops: Fact::derived(Rate::PerSecond(sparse_system / 2.0 / 8.0), cite),
506            })
507            .collect(),
508            sources: &[NV_DGX_B200_PAGE, NV_HGX_PAGE],
509        }
510    }
511
512    /// Every roofs preset, by name: the chips a decode is priced on and the spatial targets' own.
513    pub fn catalog() -> Vec<(&'static str, Roofs)> {
514        vec![
515            ("wormhole", Roofs::wormhole()),
516            ("blackhole", Roofs::blackhole()),
517            ("rtx3060", Roofs::rtx3060()),
518            ("sky130-fabric", Roofs::sky_fabric()),
519            ("a100-sxm4-80gb", Roofs::a100_sxm4_80gb()),
520            ("h100-sxm", Roofs::h100_sxm()),
521            ("h100-nvl", Roofs::h100_nvl()),
522            ("b200", Roofs::b200()),
523        ]
524    }
525
526    /// Our own fabric. Its matrix engine is not a fixed part: each program instantiates a `k` x `n` systolic
527    /// array sized by its plan's block, and the cycle twin times that array exactly, so the fabric carries no
528    /// matrix roof. What every program on it shares is the address lane, which carries one element per cycle
529    /// whatever its width. The clock is the largest signed-off array's.
530    pub fn sky_fabric() -> Roofs {
531        Roofs {
532            clock_ghz: Fact::modeled(
533                1.0 / 19.5,
534                "inherited: the largest signed-off systolic array, systolic_pipe16_realbist, closes at clock_period_ns 19.5 with \
535                 setup_worst_slack_ns 0.25 under OpenLane Sky130 STA (benchmarks/silicon/systolic_pipelined_signoff.json); a \
536                 program's own array is not signed off",
537            ),
538            mem: vec![MemRoof {
539                level: MemLevel::Dram,
540                mix: StreamMix::Any,
541                scope: Scope::Site,
542                unit: Transfer::Elements,
543                rate: Fact::derived(
544                    Rate::PerCycle(1.0),
545                    "spatial::lower::fabric::fabric_cycle_model, cycle-exact against the RTL by spatial_fabric_lowering_lock: \
546                     ReadTiles and WriteTiles take one cycle per element whatever its width",
547                ),
548            }],
549            compute: Vec::new(),
550            sources: &[FABRIC_TWIN, SKY130_SYSTOLIC_SIGNOFF],
551        }
552    }
553
554    /// The fabric on a 7-series FPGA: the same element-serial lanes as [`sky_fabric`](Self::sky_fabric), clocked from
555    /// the board.
556    pub fn xc7_fabric() -> Roofs {
557        Roofs {
558            clock_ghz: Fact::modeled(
559                0.1,
560                "the Nexys boards' 100 MHz system oscillator (Digilent Nexys A7 and Nexys Video reference manuals), the \
561                 clock an image divides down from; a program's routed Fmax is recorded beside its bitstream",
562            ),
563            sources: &[FABRIC_TWIN, DIGILENT_NEXYS_CLOCK],
564            ..Roofs::sky_fabric()
565        }
566    }
567}
568
569// ── A program's roofline ─────────────────────────────────────────────────────────────────────────────────
570
571/// A resource a program's work is bounded by.
572#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
573pub enum Resource {
574    /// The memory every site shares, over the program's whole DRAM traffic.
575    Dram,
576    /// One network link, over the busiest movement engine's traffic: an engine injects at most one flit
577    /// per cycle into the network it drives.
578    NocLink,
579    /// The matrix engine, over the busiest site's arithmetic for a per-site rate, the chip's for a
580    /// chip-wide one.
581    Matrix,
582}
583
584/// One resource's lower bound on a program's time.
585#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
586pub struct Bound {
587    pub resource: Resource,
588    /// The work the rate divides: bytes for a memory or a link, operations for an engine.
589    pub work: f64,
590    /// The rate, per cycle of the target's clock.
591    pub per_cycle: f64,
592    /// `work / per_cycle`.
593    pub cycles: f64,
594    /// The weaker of the roof's provenance and the clock's, when the roof was stated per second.
595    pub provenance: Provenance,
596}
597
598/// A program's roofline on a target: one lower bound per resource the target has a roof for, the largest
599/// of which binds. A resource with no roof has no bound; nothing is priced at a guessed rate.
600#[derive(Clone, Debug, PartialEq, Serialize)]
601pub struct Roofline {
602    pub bounds: Vec<Bound>,
603    /// The matrix operations the program performs — its arithmetic, `2 · M · N · K` for a GEMM, however many
604    /// fidelity passes the engine makes over it.
605    pub ops: f64,
606    /// The bytes it moves to and from the memory every site shares.
607    pub dram_bytes: f64,
608}
609
610impl Roofline {
611    /// The bound that binds: the resource the program waits on longest.
612    pub fn binding(&self) -> Option<&Bound> {
613        self.bounds
614            .iter()
615            .max_by(|a, b| a.cycles.total_cmp(&b.cycles))
616    }
617
618    /// No schedule of this work finishes in fewer cycles.
619    pub fn cycles(&self) -> f64 {
620        self.binding().map_or(0.0, |b| b.cycles)
621    }
622
623    /// Operations per DRAM byte, the arithmetic intensity: where the program sits on the roofline's horizontal
624    /// axis.
625    pub fn intensity(&self) -> Option<f64> {
626        (self.dram_bytes > 0.0).then(|| self.ops / self.dram_bytes)
627    }
628}
629
630/// One resource's rate per cycle, with what vouches for it.
631#[derive(Clone, Copy, Debug, PartialEq, Serialize, serde::Deserialize)]
632pub struct PerCycle {
633    pub value: f64,
634    pub provenance: Provenance,
635}
636
637/// The rate a matrix engine sustains, and what one issue the work model counts is worth against it.
638#[derive(Clone, Copy, Debug, PartialEq, Serialize, serde::Deserialize)]
639pub struct MatrixRate {
640    pub per_cycle: PerCycle,
641    /// The rate holds for the whole chip, so it bounds the whole program's arithmetic; otherwise each site's.
642    pub chip_wide: bool,
643    /// Operations one counted issue performs, one fidelity pass each — what the per-pass rate divides.
644    pub ops_per_issue: f64,
645    /// Operations of the program's arithmetic one counted issue stands for, whatever the fidelity.
646    pub arithmetic_per_issue: f64,
647}
648
649/// A memory's rate per cycle and what it counts.
650#[derive(Clone, Copy, Debug, PartialEq, Serialize, serde::Deserialize)]
651pub struct LaneRate {
652    pub per_cycle: PerCycle,
653    pub unit: Transfer,
654}
655
656/// The per-cycle rates a target's roofs give for a program whose operands are one element type: what a
657/// roofline divides the work by. A resource without a roof has no rate.
658#[derive(Clone, Copy, Debug, PartialEq, Serialize, serde::Deserialize)]
659pub struct Rates {
660    pub dram: Option<LaneRate>,
661    pub link: Option<LaneRate>,
662    pub matrix: Option<MatrixRate>,
663    /// Operations of the program's arithmetic one counted issue stands for, priced or not.
664    pub arithmetic_per_issue: f64,
665    /// Elements in one of the target's tiles: what a tile moved is worth to a lane counted in elements.
666    pub elements_per_tile: f64,
667}
668
669impl Rates {
670    /// The rates of `t`'s roofs for a program whose operands are `element`s.
671    ///
672    /// The work model counts arithmetic as `MVMUL_PER_TILE_MATMUL_PER_PASS` issues per tile-matmul pass; one
673    /// pass of a tile-matmul is `2 · rows · cols · cols` operations on the target's tile.
674    pub fn of(t: &super::target::SpatialTarget, element: Precision) -> Rates {
675        let r = &t.roofs;
676        let per = |rate: &Fact<Rate>| {
677            let c = r.per_cycle(rate);
678            PerCycle {
679                value: c.value,
680                provenance: c.provenance,
681            }
682        };
683        let ops_per_pass = 2.0 * (t.tile.rows * t.tile.cols * t.tile.cols) as f64;
684        let per_tile = super::work::mvmul_per_tile_matmul(
685            t.matmul_fidelity()
686                .unwrap_or(super::target::Fidelity::HiFi4),
687        );
688        let arithmetic_per_issue = ops_per_pass / per_tile as f64;
689        let lane = |m: &MemRoof| LaneRate {
690            per_cycle: per(&m.rate),
691            unit: m.unit,
692        };
693        Rates {
694            dram: r.mem(MemLevel::Dram, StreamMix::Any).map(lane),
695            link: r.mem(MemLevel::Noc, StreamMix::Any).map(lane),
696            matrix: r.compute(Engine::Matrix, element).map(|c| MatrixRate {
697                per_cycle: per(&c.ops),
698                chip_wide: c.scope == Scope::Chip,
699                ops_per_issue: ops_per_pass / super::work::MVMUL_PER_TILE_MATMUL_PER_PASS as f64,
700                arithmetic_per_issue,
701            }),
702            arithmetic_per_issue,
703            elements_per_tile: t.tile.elems() as f64,
704        }
705    }
706
707    /// The roofline of `w` at these rates.
708    pub fn roofline(&self, w: &super::work::Work) -> Roofline {
709        let mut bounds = Vec::new();
710        let mut bound = |resource: Resource, work: f64, rate: PerCycle| {
711            bounds.push(Bound {
712                resource,
713                work,
714                per_cycle: rate.value,
715                cycles: work / rate.value,
716                provenance: rate.provenance,
717            });
718        };
719        if let Some(lane) = self.dram {
720            let work = match lane.unit {
721                Transfer::Bytes => w.dram_bytes as f64,
722                Transfer::Elements => w.dram_tiles as f64 * self.elements_per_tile,
723            };
724            bound(Resource::Dram, work, lane.per_cycle);
725        }
726        // The work model counts the busiest movement engine's traffic in bytes only, so a link counted in
727        // elements is not priced.
728        if let Some(lane) = self.link.filter(|l| l.unit == Transfer::Bytes) {
729            bound(Resource::NocLink, w.engine_noc_bytes as f64, lane.per_cycle);
730        }
731        if let Some(m) = self.matrix {
732            let issues = if m.chip_wide {
733                w.matmul_retired
734            } else {
735                w.site_matmul_retired
736            };
737            bound(
738                Resource::Matrix,
739                issues as f64 * m.ops_per_issue,
740                m.per_cycle,
741            );
742        }
743        Roofline {
744            bounds,
745            ops: w.matmul_retired as f64 * self.arithmetic_per_issue,
746            dram_bytes: w.dram_bytes as f64,
747        }
748    }
749}
750
751/// The roofline of `w` on `t`, for a program whose operands are `element`s.
752pub fn roofline(
753    w: &super::work::Work,
754    t: &super::target::SpatialTarget,
755    element: Precision,
756) -> Roofline {
757    Rates::of(t, element).roofline(w)
758}