metrale_circuit/venn/
render.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-29: The Markdown rendering of a [`VennReport`]: the report the `new-model` method
4//! checks in as `kernels/circuits/venn/<target>-vs-<arch>.md`.
5//!
6//! Owner: metrale-circuit (venn).
7//! Invariants: the text is a pure function of the report; numbers print at fixed precision, so
8//! a regenerated report is byte-identical and `--check` can compare it.
9
10use std::fmt::Write as _;
11
12use super::families::{EvidenceSource, ParamKind};
13use super::report::{Row, VennReport};
14use super::{Class, Compared, Finding};
15use crate::ir::LayerKind;
16
17fn short(recipe: &str) -> &str {
18    recipe.rsplit('/').next().unwrap_or(recipe)
19}
20
21fn base(path: &str) -> &str {
22    path.rsplit('/').next().unwrap_or(path)
23}
24
25fn compared(f: &Finding) -> String {
26    match &f.compared {
27        Compared::Model {
28            recipe,
29            node,
30            kernels,
31        } => format!("{}: `{kernels}` ({} `{node}`)", f.family, short(recipe)),
32        Compared::Point { how, files } => {
33            let more = if files.len() > 1 {
34                format!(" +{}", files.len() - 1)
35            } else {
36                String::new()
37            };
38            format!(
39                "{}: {} in `{}`{more}; no compared model runs this op on it here",
40                f.family,
41                how.name(),
42                base(&files[0])
43            )
44        }
45    }
46}
47
48fn diffs(f: &Finding) -> (String, String) {
49    if f.diffs.is_empty() {
50        return ("none".into(), "-".into());
51    }
52    let what: Vec<String> = f
53        .diffs
54        .iter()
55        .map(|d| format!("{} {} vs {}", d.param, d.target, d.other))
56        .collect();
57    let kinds: Vec<&str> = f.diffs.iter().map(|d| d.kind.name()).collect();
58    (what.join("; "), kinds.join("; "))
59}
60
61fn evidence(f: &Finding, r: &VennReport) -> String {
62    if f.evidence.is_empty() {
63        return "none".into();
64    }
65    f.evidence
66        .iter()
67        .map(|e| match e {
68            EvidenceSource::Measurement(k) => match r.cited.get(k) {
69                Some(p) => format!("`{k}` ({p:.1}% of floor)"),
70                None => format!("`{k}`"),
71            },
72            EvidenceSource::Microbench(m) => format!("microbench: {m}"),
73        })
74        .collect::<Vec<_>>()
75        .join("; ")
76}
77
78fn pointed(f: &Finding, kind: ParamKind) -> Vec<&str> {
79    f.diffs
80        .iter()
81        .filter(|d| d.kind == kind)
82        .map(|d| d.param.as_str())
83        .collect()
84}
85
86fn decision(row: &Row) -> String {
87    let Some(f) = &row.primary else {
88        return "build: reference kernel, logits parity, then microbench against its roofline"
89            .into();
90    };
91    let mut out = match f.class {
92        Class::Shared => "reuse (optimized at this point)".to_string(),
93        Class::SharedUnmeasured => "reuse, then microbench at this point".to_string(),
94        Class::PolicyVariant => format!(
95            "policy template on {} (or split + bit_identical fusion rule)",
96            pointed(f, ParamKind::Policy).join(", ")
97        ),
98        Class::ParameterizationOpportunity => {
99            let mut s = format!("template on {}", pointed(f, ParamKind::Compile).join(", "));
100            let pol = pointed(f, ParamKind::Policy);
101            if !pol.is_empty() {
102                let _ = write!(s, " + policy {}", pol.join(", "));
103            }
104            s.push_str("; stability gate at existing points");
105            s
106        }
107        Class::Novel => String::new(),
108    };
109    if f.instantiated == Some(super::families::How::Copy) {
110        out.push_str("; the target point exists only as a file copy");
111    }
112    for a in &row.also {
113        let params: Vec<&str> = a
114            .diffs
115            .iter()
116            .filter(|d| d.kind != ParamKind::Runtime)
117            .map(|d| d.param.as_str())
118            .collect();
119        let _ = write!(
120            out,
121            "; also {}: {} ({})",
122            a.family,
123            a.class.name(),
124            params.join(", ")
125        );
126    }
127    out
128}
129
130fn kind_name(k: Option<LayerKind>) -> &'static str {
131    k.map_or("head / draft", LayerKind::name)
132}
133
134/// 2026-09-29: Render `r` as Markdown.
135pub fn render(r: &VennReport) -> String {
136    let mut s = String::new();
137    let t = &r.target;
138    let against: Vec<&str> = r.against.iter().map(|a| a.checkpoint.as_str()).collect();
139    let _ = writeln!(
140        s,
141        "# Kernel Venn: {} vs {}\n",
142        t.checkpoint,
143        against.join(", ")
144    );
145    let _ = writeln!(
146        s,
147        "Generated by `{}`. Do not edit: regenerate with that command, and `--check` verifies it is current.\n",
148        r.command
149    );
150    s.push_str("## Target\n\n| | |\n|---|---|\n");
151    let _ = writeln!(s, "| Recipe | `{}` |", t.recipe);
152    let _ = writeln!(s, "| Checkpoint | `{}` |", t.checkpoint);
153    let _ = writeln!(s, "| Circuit | `kernels/circuits/{}.toml` |", r.arch);
154    let _ = writeln!(s, "| Kernel target | `{}` |", t.target);
155    let mut kinds: Vec<(LayerKind, usize)> = Vec::new();
156    for k in &r.layer_kinds {
157        match kinds.iter_mut().find(|(x, _)| x == k) {
158            Some((_, n)) => *n += 1,
159            None => kinds.push((*k, 1)),
160        }
161    }
162    let kinds: Vec<String> = kinds
163        .iter()
164        .map(|(k, n)| format!("{n} {}", k.name()))
165        .collect();
166    let _ = writeln!(
167        s,
168        "| Layers | {}: {} |",
169        r.layer_kinds.len(),
170        kinds.join(", ")
171    );
172    let dims: Vec<String> = t
173        .shape
174        .dims
175        .iter()
176        .map(|(k, v)| format!("{k} {v}"))
177        .collect();
178    let _ = writeln!(s, "| Dims | {} |", dims.join(", "));
179    let set: Vec<String> = t
180        .policy
181        .settings
182        .iter()
183        .map(|(k, v)| format!("{k}={v}"))
184        .collect();
185    let _ = writeln!(s, "| Settings | {} |\n", set.join(" "));
186    s.push_str("### Declared precision (weight / input activation per site)\n\n| Site | Weight | Activation |\n|---|---|---|\n");
187    for (site, (w, a)) in &r.precision {
188        let _ = writeln!(s, "| `{site}` | {w} | {a} |");
189    }
190    s.push_str(
191        "\n## Compared with\n\n| Recipe | Checkpoint | Circuit | Settings |\n|---|---|---|---|\n",
192    );
193    for a in &r.against {
194        let set: Vec<String> = a
195            .policy
196            .settings
197            .iter()
198            .map(|(k, v)| format!("{k}={v}"))
199            .collect();
200        let _ = writeln!(
201            s,
202            "| `{}` | `{}` | `{}` | {} |",
203            a.recipe,
204            a.checkpoint,
205            a.arch,
206            set.join(" ")
207        );
208    }
209    let rf = &r.roofline;
210    let _ = writeln!(
211        s,
212        "\n## Estimate\n\nStep share is a roofline estimate from edge shapes and formats, per node: \
213         max(bytes / {:.1} GB/s, FLOPs / peak), with peaks BF16 {:.1}, FP8 {:.1} and NVFP4 {:.1} TFLOPS \
214         (the MMA class of the node's input activation). State traffic: the paged KV cache over {} \
215         tokens per sequence, and FP32 recurrent states read and written once per sequence. Routed \
216         experts read E(1 - (1 - k/E)^T) distinct experts for T tokens. Launch overhead and conv \
217         windows are not counted. Replace with measured profiles once a serve runs.\n",
218        rf.dram_gbps, rf.bf16_tflops, rf.fp8_tflops, rf.nvfp4_tflops, rf.context_tokens
219    );
220    s.push_str("## Flags: layers that cannot batch rows\n\n");
221    if r.flags.is_empty() {
222        s.push_str("None.\n");
223    } else {
224        s.push_str("| # | Layer kind | Mode | Rows | Est. added time | vs. multi-row step | Source | Note |\n|---|---|---|---|---|---|---|---|\n");
225        for (i, f) in r.flags.iter().enumerate() {
226            let _ = writeln!(
227                s,
228                "| {} | {} | {} | {} | {:.1} µs | +{:.1}% | {} | {} |",
229                i + 1,
230                kind_name(f.layer_kind),
231                f.run.mode.name(),
232                f.run.rows,
233                f.added_us,
234                100.0 * f.share,
235                f.source,
236                f.note
237            );
238        }
239    }
240    for tb in &r.tables {
241        let _ = writeln!(
242            s,
243            "\n## {}, {} row{} (estimated step {:.1} µs)\n",
244            tb.run.mode.name(),
245            tb.run.rows,
246            if tb.run.rows == 1 { "" } else { "s" },
247            tb.total_us
248        );
249        s.push_str("| # | Phase | Target op | Comparison kernel | Class | Parameter(s) differing | Runtime / compile-time / policy | Est. step share | Evidence at target point | Decision |\n|---|---|---|---|---|---|---|---|---|---|\n");
250        for (i, row) in tb.rows.iter().enumerate() {
251            let fmt = match (&row.weight, &row.activation) {
252                (Some(w), Some(a)) => format!(", W {w}, A {a}"),
253                (None, Some(a)) => format!(", in {a}"),
254                _ => String::new(),
255            };
256            let (cmp, class, (what, kinds), ev) = match &row.primary {
257                Some(f) => (compared(f), f.class.name(), diffs(f), evidence(f, r)),
258                None => (
259                    "no family implements it".to_string(),
260                    Class::Novel.name(),
261                    ("-".to_string(), "-".to_string()),
262                    "none".to_string(),
263                ),
264            };
265            let _ = writeln!(
266                s,
267                "| {} | {} | `{}` ×{} (`{}`{fmt}) | {cmp} | {class} | {what} | {kinds} | {:.1}% ({:.1} µs) | {ev} | {} |",
268                i + 1,
269                tb.run.mode.name(),
270                row.site,
271                row.count,
272                row.op,
273                100.0 * row.share,
274                row.time_us,
275                decision(row)
276            );
277        }
278    }
279    super::render_summary::summary(r, &mut s);
280    s
281}