metrale_circuit/hardware/
render.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-30: The Markdown of a hardware report (`kernels/circuits/plans/hw/<model>--<device>.md`)
4//! and its one-line summary for the roadmap matrix.
5//!
6//! Owner: metrale-circuit (hardware).
7//! Invariants: deterministic text (fixed decimals, sorted maps); every number names its basis in
8//! the report's inputs table.
9
10use std::collections::BTreeMap;
11use std::fmt::Write as _;
12
13use super::HwReport;
14use super::gaps::GapTable;
15use super::plan::NOVEL_EMITTER;
16use crate::venn::Class;
17
18/// 2026-09-30: The C values of the memory-fit table.
19pub(super) const FIT_SEQS: [u64; 8] = [1, 2, 4, 8, 16, 32, 64, 128];
20/// 2026-09-30: The contexts of the memory-fit table.
21pub(super) const FIT_CONTEXTS: [u64; 2] = [4096, 32768];
22
23fn gb(b: f64) -> String {
24    format!("{:.2}", b / 1e9)
25}
26
27/// 2026-10-01: The label every number estimated from `r`'s constants carries when they are not
28/// measured on the device's own class (a datasheet ceiling): `" (roofline projection,
29/// unmeasured)"`, else nothing.
30pub(super) fn projection(r: &HwReport) -> &'static str {
31    if r.resolved.roofline.measured {
32        ""
33    } else {
34        " (roofline projection, unmeasured)"
35    }
36}
37
38pub(super) fn pct(x: f64) -> String {
39    // 2026-09-30: `+ 0.0` turns the -0.0 an empty f64 sum yields into 0.0.
40    format!("{:.1}%", 100.0 * x + 0.0)
41}
42
43fn run_name(t: &GapTable) -> String {
44    format!("{} n={}", t.planned.plan.mode.name(), t.planned.plan.rows)
45}
46
47/// 2026-09-30: The report text.
48pub fn render_report(r: &HwReport) -> String {
49    let mut s = String::new();
50    let d = &r.resolved.device;
51    let _ = writeln!(s, "# {} on {} (`{}`)\n", r.model.label, d.name, d.id);
52    let _ = writeln!(
53        s,
54        "Generated by `{}`. Do not edit: the `--check` test fails on a stale copy.\n",
55        r.command
56    );
57    inputs(&mut s, r);
58    execution(&mut s, r);
59    super::tc_policy_render::section(&mut s, r);
60    estimates(&mut s, r);
61    super::render_routes::routes_section(&mut s, r);
62    memory(&mut s, r);
63    flags(&mut s, r);
64    if let Some(t) = r.tables.first() {
65        plan_sites(&mut s, t, r);
66    }
67    for t in &r.tables {
68        gap_rows(&mut s, t, projection(r));
69    }
70    absent(&mut s, r);
71    s
72}
73
74fn inputs(s: &mut String, r: &HwReport) {
75    let d = &r.resolved.device;
76    let rf = &r.resolved.roofline;
77    let chain: Vec<&str> = r
78        .resolved
79        .planning
80        .iter()
81        .map(|c| c.name.as_str())
82        .collect();
83    let rules = match &r.resolved.rules {
84        super::class::ClassRules::Rules {
85            files,
86            rules,
87            runtime,
88        } => {
89            let routes = match runtime.len() {
90                0 => String::new(),
91                n => format!(" and {n} runtime route{}", if n == 1 { "" } else { "s" }),
92            };
93            format!("{} rules{routes} from {}", rules.len(), files.join(" + "))
94        }
95        super::class::ClassRules::None => {
96            "none: the class declares no FUSIONS.toml and inherits none, so every node is a gap"
97                .into()
98        }
99    };
100    let target = match &r.resolved.sources.target {
101        Some(t) => format!("`{t}` ({} modules)", r.resolved.sources.modules.len()),
102        None => format!(
103            "common layer only ({} modules): kernels/{} has no `{}/{}` target",
104            r.resolved.sources.modules.len(),
105            d.class,
106            r.model.kernel_model,
107            r.model.kernel_quant
108        ),
109    };
110    let native: Vec<String> = d.native.iter().map(|i| i.name()).collect();
111    let class_set: Vec<String> = r
112        .class_settings
113        .iter()
114        .map(|(k, v)| format!("{k}={v}"))
115        .collect();
116    let _ = writeln!(s, "## Inputs\n\n| | |\n|---|---|");
117    let rows = [
118        ("model", r.model.label.clone()),
119        ("checkpoint", r.model.checkpoint.clone()),
120        ("formats", r.model.precision.clone()),
121        (
122            "device",
123            format!(
124                "{} ({}, CC {}, {} SMs, {:.1} GiB {} visible, {} GB/s, L2 {}, {} KB smem/SM, cluster {}, TMEM {} KB/SM)",
125                d.name,
126                d.arch,
127                d.compute_capability,
128                d.sms,
129                d.memory_bytes / (1u64 << 30) as f64,
130                d.memory_type,
131                d.bandwidth_gbps,
132                d.l2_mb
133                    .map_or_else(|| "unpublished".into(), |l| format!("{l} MB")),
134                d.smem_per_sm_kb,
135                d.cluster_max,
136                d.tmem_per_sm_kb
137            ),
138        ),
139        (
140            "tensor cores",
141            format!(
142                "{} (fast path); native: {}",
143                d.mma_family.name(),
144                native.join(", ")
145            ),
146        ),
147        (
148            "roofline",
149            format!(
150                "{}: DRAM {} GB/s, BF16 {} / FP8 {} / NVFP4 {} TFLOPS; KV context {} tokens per sequence (assumption)",
151                rf.basis,
152                rf.roofline.dram_gbps,
153                rf.roofline.bf16_tflops,
154                rf.roofline.fp8_tflops,
155                rf.roofline.nvfp4_tflops,
156                rf.roofline.context_tokens
157            ),
158        ),
159        ("FP4 costing", r.resolved.fp4_costing(&r.model.circuit)),
160        ("class chain", chain.join(" -> ")),
161        ("rules", rules),
162        ("kernel sources", target),
163        (
164            "settings from the class",
165            if class_set.is_empty() {
166                "none differ from the model's source".into()
167            } else {
168                format!(
169                    "{} (kernels/{}/HARDWARE.toml [defaults])",
170                    class_set.join(" "),
171                    d.class
172                )
173            },
174        ),
175    ];
176    let policy: Vec<String> = r
177        .policy
178        .settings
179        .iter()
180        .map(
181            |(k, v)| match r.model.policy_sources.iter().find(|(key, _)| key == k) {
182                Some((_, why)) => format!("{k}={v} ({why})"),
183                None => format!("{k}={v}"),
184            },
185        )
186        .collect();
187    for (k, v) in rows.into_iter().chain([(
188        "policy",
189        if r.model.policy_sources.is_empty() {
190            format!("{} (stated by the recipe)", policy.join(" "))
191        } else {
192            policy.join("; ")
193        },
194    )]) {
195        let _ = writeln!(s, "| {k} | {} |", v.replace('|', "\\|"));
196    }
197    let _ = writeln!(s);
198}
199
200fn execution(s: &mut String, r: &HwReport) {
201    let _ = writeln!(
202        s,
203        "## Declared formats on this device\n\nThe checkpoint's formats are kept; this is how the device and its class's compiled kernels run them. A pair whose nodes run differently has one row per execution.\n"
204    );
205    let _ = writeln!(
206        s,
207        "| pair | weight | activation | nodes | execution |\n|---|---|---|---:|---|"
208    );
209    for ((pair, w, a, e), n) in &r.exec {
210        let _ = writeln!(s, "| {pair} | {w} | {a} | {n} | {} |", e.describe());
211    }
212    let _ = writeln!(s);
213}
214
215fn estimates(s: &mut String, r: &HwReport) {
216    let bw = r.resolved.roofline.roofline.dram_gbps;
217    let _ = writeln!(
218        s,
219        "## Roofline estimates\n\nSums of per-node `max(bytes / bandwidth, FLOPs / peak)`; per-row loops are costed per row. No launch overhead: a ceiling, not a prediction.\n"
220    );
221    let _ = writeln!(s, "| case | time (ms) | tok/s |\n|---|---:|---:|");
222    for t in &r.tables {
223        let rows = t.planned.plan.rows;
224        let _ = writeln!(
225            s,
226            "| decode C={rows} ({}){} | {:.3} | {:.1} |",
227            run_name(t),
228            projection(r),
229            t.total_us / 1e3,
230            rows as f64 / (t.total_us / 1e6)
231        );
232    }
233    for (tokens, us) in &r.prefill {
234        let _ = writeln!(
235            s,
236            "| prefill {}k{} | {:.1} | {:.0} |",
237            tokens / 1024,
238            projection(r),
239            us / 1e3,
240            *tokens as f64 / (us / 1e6)
241        );
242    }
243    let floor_us = r.weight_floor / (bw * 1e3);
244    let _ = writeln!(
245        s,
246        "\nWeight floor at C=1: {} GB read per step = {:.3} ms at {bw} GB/s{}.\n",
247        gb(r.weight_floor),
248        floor_us / 1e3,
249        projection(r)
250    );
251}
252
253fn memory(s: &mut String, r: &HwReport) {
254    let d = &r.resolved.device;
255    let usable = d.memory_bytes * d.usable_fraction;
256    let f = &r.footprint;
257    let _ = writeln!(
258        s,
259        "## Memory fit\n\nUsable {} GB = {:.2} GB visible x {} ({}). Weights {} GB (embedding BF16); KV {:.0} bytes per token; recurrent state {:.1} MB per sequence. Not counted: activations, workspaces, conv windows, the CUDA context.\n",
260        gb(usable),
261        d.memory_bytes / 1e9,
262        d.usable_fraction,
263        d.usable_why,
264        gb(f.weights),
265        f.kv_per_token,
266        f.state_per_seq / 1e6
267    );
268    let mut head = String::from("| C |");
269    let mut rule = String::from("|---:|");
270    for ctx in FIT_CONTEXTS {
271        let _ = write!(head, " {}k GB | fits |", ctx / 1024);
272        rule.push_str("---:|---|");
273    }
274    let _ = writeln!(s, "{head}\n{rule}");
275    for c in FIT_SEQS {
276        let mut line = format!("| {c} |");
277        for ctx in FIT_CONTEXTS {
278            let need = f.bytes(c, ctx);
279            let _ = write!(
280                line,
281                " {} | {} |",
282                gb(need),
283                if need <= usable { "yes" } else { "no" }
284            );
285        }
286        let _ = writeln!(s, "{line}");
287    }
288    let _ = writeln!(s);
289    for c in [1, 128] {
290        let need = f.bytes(c, FIT_CONTEXTS[0]);
291        if need > usable {
292            let _ = writeln!(s, "C={c} at 4k does not fit: {}.", advice(r, need, usable));
293        }
294    }
295    let _ = writeln!(s);
296}
297
298/// 2026-09-30: What would hold `need` bytes: tensor parallelism over this device, or the
299/// registry devices whose usable memory holds it.
300fn advice(r: &HwReport, need: f64, usable: f64) -> String {
301    let tp = (need / usable).ceil() as u64;
302    let fits: Vec<&str> = r
303        .others
304        .iter()
305        .filter(|(_, u)| *u >= need)
306        .map(|(id, _)| id.as_str())
307        .collect();
308    let others = if fits.is_empty() {
309        "no single registry device holds it".to_string()
310    } else {
311        format!("or one {}", fits.join(" / "))
312    };
313    format!(
314        "needs {} GB of {} GB usable: TP={tp} on {}, {others}",
315        gb(need),
316        gb(usable),
317        r.resolved.device.id
318    )
319}
320
321fn flags(s: &mut String, r: &HwReport) {
322    let _ = writeln!(
323        s,
324        "## Multi-row fallbacks\n\nPlan groups that loop once per row, and the engine's layer loops per sequence (`legacy` rows, KERNEL_FAMILIES.toml `[[legacy_path]]`), costed as the time the loop adds over one multi-row launch{}; the estimates above include it.\n",
325        projection(r)
326    );
327    let mut any = false;
328    for t in &r.tables {
329        for f in &t.flags {
330            if !any {
331                let _ = writeln!(
332                    s,
333                    "| run | layer kind | added (ms) | share | sites | rules |\n|---|---|---:|---:|---|---|"
334                );
335                any = true;
336            }
337            let kind = f.layer_kind.map_or("prologue/epilogue", |k| k.name());
338            let sites: Vec<&str> = f.sites.iter().map(String::as_str).collect();
339            let rules: Vec<&str> = f.rules.iter().map(String::as_str).collect();
340            let _ = writeln!(
341                s,
342                "| {} | {kind} | {:.3} | {} | {} | {} |",
343                run_name(t),
344                f.added_us / 1e3,
345                pct(f.share),
346                sites.join(", "),
347                rules.join(", ")
348            );
349        }
350    }
351    if !any {
352        let _ = writeln!(s, "None.");
353    }
354    let _ = writeln!(s);
355}
356
357fn plan_sites(s: &mut String, t: &GapTable, r: &HwReport) {
358    let p = &t.planned.plan;
359    let c = &r.model.circuit;
360    let fused = p
361        .edge_states
362        .iter()
363        .filter(|e| matches!(e, Some(crate::fuser::EdgeState::Fused(_))))
364        .count();
365    let novel_groups = p
366        .groups
367        .iter()
368        .filter(|g| g.emitter == NOVEL_EMITTER)
369        .count();
370    let _ = writeln!(
371        s,
372        "## Fused plan: {}\n\ndigest {}; {} groups, {} launches, {} fused edges, {} placeholder (novel) groups. The full plan: `met circuit plan --checkpoint {} --hardware {} --precision {} --format plan`.\n",
373        run_name(t),
374        p.digest,
375        p.groups.len(),
376        p.launches(),
377        fused,
378        novel_groups,
379        r.model.checkpoint,
380        r.resolved.device.id,
381        r.model.precision_choice.name()
382    );
383    let mut sites: BTreeMap<(String, String, String), usize> = BTreeMap::new();
384    for g in &p.groups {
385        let first = &c.nodes[g.nodes[0]];
386        let locals: Vec<&str> = g.nodes.iter().map(|&n| c.nodes[n].local.as_str()).collect();
387        let what = if g.kernels.is_empty() {
388            format!("({})", g.emitter)
389        } else {
390            let k: Vec<String> = g.kernels.iter().map(|k| k.to_string()).collect();
391            k.join(" + ")
392        };
393        let key = (
394            format!("{} [{}]", first.block, locals.join(",")),
395            what,
396            g.rule.clone(),
397        );
398        *sites.entry(key).or_insert(0) += 1;
399    }
400    for (rule, why) in &t.planned.refused {
401        let _ = writeln!(s, "Rule `{rule}` left out of this plan: {why}.\n");
402    }
403    let _ = writeln!(s, "| site | kernels | rule | groups |\n|---|---|---|---:|");
404    for ((site, what, rule), n) in sites {
405        let _ = writeln!(s, "| {site} | {what} | {rule} | {n} |");
406    }
407    let _ = writeln!(s);
408}
409
410fn gap_rows(s: &mut String, t: &GapTable, projection: &str) {
411    let _ = writeln!(
412        s,
413        "## Gap report: {}\n\nEstimated step {:.3} ms{projection}. Shared {} (measured on this class), shared-unmeasured {}, parameterisation {}, policy variant {}, novel {} of the step.\n",
414        run_name(t),
415        t.total_us / 1e3,
416        pct(t.share_of(&[Class::Shared])),
417        pct(t.share_of(&[Class::SharedUnmeasured])),
418        pct(t.share_of(&[Class::ParameterizationOpportunity])),
419        pct(t.share_of(&[Class::PolicyVariant])),
420        pct(t.share_of(&[Class::Novel]))
421    );
422    let _ = writeln!(
423        s,
424        "| site | op | formats | execution | n | share | class | family | detail |\n|---|---|---|---|---:|---:|---|---|---|"
425    );
426    for row in &t.rows {
427        let formats = row
428            .formats
429            .as_ref()
430            .map_or_else(|| "-".to_string(), |(w, a)| format!("{w} x {a}"));
431        let exec = row.exec.map_or_else(|| "-".to_string(), |e| e.describe());
432        let mut detail = row.detail.clone();
433        if !row.diffs.is_empty() {
434            let _ = write!(detail, "; differs: {}", row.diffs.join(", "));
435        }
436        let _ = writeln!(
437            s,
438            "| {} | {} | {formats} | {exec} | {} | {} | {} | {} | {} |",
439            row.site,
440            row.op,
441            row.count,
442            pct(row.share),
443            row.class.name(),
444            row.family.as_deref().unwrap_or("-"),
445            detail.replace('|', "\\|")
446        );
447    }
448    let _ = writeln!(s);
449}
450
451fn absent(s: &mut String, r: &HwReport) {
452    let _ = writeln!(
453        s,
454        "## Rule kernels this device cannot run\n\nKernels a rule under this policy names that the device's class does not provide: the port list.\n"
455    );
456    if r.absent.is_empty() {
457        let _ = writeln!(s, "None.");
458        return;
459    }
460    let _ = writeln!(s, "| kernel | why |\n|---|---|");
461    for (k, a) in &r.absent {
462        let _ = writeln!(s, "| {k} | {} |", a.describe().replace('|', "\\|"));
463    }
464}