1use 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
18pub(super) const FIT_SEQS: [u64; 8] = [1, 2, 4, 8, 16, 32, 64, 128];
20pub(super) const FIT_CONTEXTS: [u64; 2] = [4096, 32768];
22
23fn gb(b: f64) -> String {
24 format!("{:.2}", b / 1e9)
25}
26
27pub(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 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
47pub 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
298fn 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}