metrale_circuit/hardware/
gaps.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-30: The Venn/gap table of one plan on one device: every node of the mode's section,
4//! grouped by site (a template node across its layers), classified against the families the
5//! device can run, and ranked by its share of the device's estimated step time.
6//!
7//! Owner: metrale-circuit (hardware).
8//! Invariants:
9//! - One classifier: `venn::classify::classify_node`, over the class's families (points
10//!   re-resolved on the class, evidence only where measured on it). A planned group is
11//!   classified through its kernels; a placeholder group through the families that implement
12//!   the op, and is Novel when none does.
13//! - A site's execution line states how the device runs the node's DECLARED formats
14//!   ([`super::exec`]); nothing is re-planned at a wider format.
15//! - Flags are plan groups that loop per row at more than one row, and the layer loops the
16//!   families manifest records for the engine's code (`[[legacy_path]]`), each costed as the
17//!   time the loop adds over one multi-row launch; the step estimate includes that time.
18
19use std::collections::{BTreeMap, BTreeSet};
20
21use super::HwError;
22use super::estimate::activation_of;
23use super::exec::Exec;
24use super::plan::{NOVEL_EMITTER, Planned, Resolved};
25use crate::fuser::section_of;
26use crate::ir::{Circuit, LayerKind, NodeIdx};
27use crate::rules::Repeat;
28use crate::venn::classify::{candidates, classify_node};
29use crate::venn::roofline::node_cost;
30use crate::venn::{Class, Subject, VennError};
31
32/// 2026-09-30: One site.
33#[derive(Debug, Clone)]
34pub struct GapRow {
35    /// 2026-09-30: `block.node`.
36    pub site: String,
37    /// 2026-09-30: Op name.
38    pub op: String,
39    /// 2026-09-30: Declared weight and activation, for weight-reading ops.
40    pub formats: Option<(String, String)>,
41    /// 2026-09-30: How the device executes the declared formats (weight-reading ops).
42    pub exec: Option<Exec>,
43    /// 2026-09-30: Nodes (layers).
44    pub count: usize,
45    /// 2026-09-30: Estimated time, microseconds.
46    pub time_us: f64,
47    /// 2026-09-30: Share of the step.
48    pub share: f64,
49    /// 2026-09-30: Class.
50    pub class: Class,
51    /// 2026-09-30: Family of the primary finding.
52    pub family: Option<String>,
53    /// 2026-09-30: What runs it (kernels or rule), or why nothing does.
54    pub detail: String,
55    /// 2026-09-30: Differing parameters of the primary finding.
56    pub diffs: Vec<String>,
57}
58
59/// 2026-09-30: A group that loops per row.
60#[derive(Debug, Clone)]
61pub struct Flag {
62    /// 2026-09-30: Layer kind (`None`: prologue / epilogue).
63    pub layer_kind: Option<LayerKind>,
64    /// 2026-09-30: Sites.
65    pub sites: BTreeSet<String>,
66    /// 2026-09-30: Rules.
67    pub rules: BTreeSet<String>,
68    /// 2026-09-30: Time the loop adds, microseconds.
69    pub added_us: f64,
70    /// 2026-09-30: Over the step.
71    pub share: f64,
72}
73
74/// 2026-09-30: One plan's table.
75#[derive(Debug, Clone)]
76pub struct GapTable {
77    /// 2026-09-30: The plan.
78    pub planned: Planned,
79    /// 2026-09-30: Estimated step, microseconds (per-row loops included).
80    pub total_us: f64,
81    /// 2026-09-30: Sites, by descending share.
82    pub rows: Vec<GapRow>,
83    /// 2026-09-30: Per-row fallbacks, by descending cost.
84    pub flags: Vec<Flag>,
85}
86
87impl GapTable {
88    /// 2026-09-30: Share of the step whose class is one of `classes`.
89    pub fn share_of(&self, classes: &[Class]) -> f64 {
90        self.rows
91            .iter()
92            .filter(|r| classes.contains(&r.class))
93            .map(|r| r.share)
94            .sum()
95    }
96}
97
98fn site_of(c: &Circuit, n: NodeIdx) -> String {
99    format!("{}.{}", c.nodes[n].block, c.nodes[n].local)
100}
101
102/// 2026-09-30: Build the table of `planned` for the circuit `c` under `settings` on `r`.
103pub fn gap_table(
104    r: &Resolved,
105    c: &Circuit,
106    settings: &BTreeMap<String, String>,
107    recipe: &str,
108    planned: Planned,
109) -> Result<GapTable, HwError> {
110    let plan = &planned.plan;
111    let (mode, rows) = (plan.mode, plan.rows);
112    let with_plan = Subject {
113        recipe,
114        circuit: c,
115        settings,
116        plan: Some(plan),
117    };
118    let bare = Subject {
119        plan: None,
120        ..with_plan
121    };
122    let group_of: BTreeMap<NodeIdx, usize> = plan
123        .groups
124        .iter()
125        .enumerate()
126        .flat_map(|(g, grp)| grp.nodes.iter().map(move |&n| (n, g)))
127        .collect();
128    let section = section_of(mode);
129    let scope: Vec<NodeIdx> = c
130        .blocks
131        .iter()
132        .filter(|b| b.section == section)
133        .flat_map(|b| b.first..b.end)
134        .collect();
135    let mut by_site: BTreeMap<(String, String), GapRow> = BTreeMap::new();
136    let mut flags: BTreeMap<Option<LayerKind>, Flag> = BTreeMap::new();
137    let mut total = 0.0;
138    for &n in &scope {
139        let node = &c.nodes[n];
140        let g = &plan.groups[*group_of
141            .get(&n)
142            .ok_or_else(|| HwError::Plan(format!("node `{}` is in no group", node.id)))?];
143        let rf = r.roofline_of(n);
144        let one = node_cost(c, node, mode, 1, settings, &rf)?.time_us;
145        let all = node_cost(c, node, mode, rows, settings, &rf)?.time_us;
146        let kind = node.layer.map(|l| c.layer_kinds[l]);
147        let site = site_of(c, n);
148        // 2026-09-30: The engine's layer code loops per sequence here (a KERNEL_FAMILIES.toml
149        // legacy fact, the same Rust on every class), whatever the plan groups say.
150        let legacy = r.families.legacy.iter().find(|l| {
151            l.arch == c.arch
152                && l.per_sequence.contains(&mode)
153                && rows > l.rows_above
154                && Some(l.layer_kind) == kind
155                && (l.sites.is_empty() || l.sites.contains(&site))
156        });
157        let looped = matches!(g.repeat, Repeat::PerRow | Repeat::PerRowButLast) && rows > 1;
158        let per_row = looped || legacy.is_some();
159        let time = if per_row { rows as f64 * one } else { all };
160        total += time;
161        if per_row {
162            let f = flags.entry(kind).or_insert_with(|| Flag {
163                layer_kind: kind,
164                sites: BTreeSet::new(),
165                rules: BTreeSet::new(),
166                added_us: 0.0,
167                share: 0.0,
168            });
169            f.sites.insert(site.clone());
170            f.rules.insert(match legacy.filter(|_| !looped) {
171                Some(l) => format!("legacy {}", l.cite),
172                None => g.rule.clone(),
173            });
174            f.added_us += time - all;
175        }
176        let act = activation_of(c, node);
177        let formats = node.weight.zip(act).map(|(w, a)| (w.name(), a.name()));
178        let key = (site, format!("{formats:?}"));
179        if let Some(row) = by_site.get_mut(&key) {
180            row.count += 1;
181            row.time_us += time;
182            continue;
183        }
184        let novel = g.emitter == NOVEL_EMITTER;
185        let classified = if novel {
186            classify_node(&bare, n, rows, &[], &r.families)
187        } else {
188            classify_node(&with_plan, n, rows, &[], &r.families)
189        };
190        let (class, family, diffs, detail) = match classified {
191            Ok((Some(f), _)) => {
192                let detail = if novel {
193                    format!(
194                        "no rule of this class covers it; family `{}` implements the op",
195                        f.family
196                    )
197                } else {
198                    kernels_of(g, &r.families)
199                };
200                let diffs = f
201                    .diffs
202                    .iter()
203                    .map(|d| format!("{} {}->{}", d.param, d.other, d.target))
204                    .collect();
205                // 2026-09-30: A family that no rule of the class selects has no evidence in
206                // this plan, whatever it measured elsewhere.
207                let class = if novel && f.class == Class::Shared {
208                    Class::SharedUnmeasured
209                } else {
210                    f.class
211                };
212                (class, Some(f.family), diffs, detail)
213            }
214            Ok((None, _)) => {
215                let one_row: Vec<&str> = candidates(&r.families, c, node, 1)
216                    .into_iter()
217                    .map(|f| f.id.as_str())
218                    .collect();
219                let detail = if one_row.is_empty() {
220                    "no family available on this device implements it".to_string()
221                } else {
222                    format!(
223                        "no rule of this class covers it; {} run it one row per launch only",
224                        one_row.join(", ")
225                    )
226                };
227                (Class::Novel, None, Vec::new(), detail)
228            }
229            Err(VennError::UnmappedKernel { kernels, .. }) => (
230                Class::SharedUnmeasured,
231                None,
232                Vec::new(),
233                format!("{kernels} (in no kernel family)"),
234            ),
235            Err(VennError::Load(msg)) if !novel => (
236                Class::SharedUnmeasured,
237                None,
238                Vec::new(),
239                format!("{} (unclassified: {msg})", kernels_of(g, &r.families)),
240            ),
241            Err(e) => return Err(HwError::Plan(e.to_string())),
242        };
243        by_site.insert(
244            key.clone(),
245            GapRow {
246                site: key.0,
247                op: node.op.name(),
248                exec: r.exec[n],
249                formats,
250                count: 1,
251                time_us: time,
252                share: 0.0,
253                class,
254                family,
255                detail,
256                diffs,
257            },
258        );
259    }
260    let mut rows_out: Vec<GapRow> = by_site.into_values().collect();
261    for row in &mut rows_out {
262        row.share = if total > 0.0 {
263            row.time_us / total
264        } else {
265            0.0
266        };
267    }
268    rows_out.sort_by(|a, b| {
269        b.time_us
270            .total_cmp(&a.time_us)
271            .then_with(|| a.site.cmp(&b.site))
272    });
273    let mut flags: Vec<Flag> = flags.into_values().collect();
274    for f in &mut flags {
275        f.share = if total > 0.0 { f.added_us / total } else { 0.0 };
276    }
277    flags.sort_by(|a, b| b.added_us.total_cmp(&a.added_us));
278    Ok(GapTable {
279        planned,
280        total_us: total,
281        rows: rows_out,
282        flags,
283    })
284}
285
286/// 2026-09-30: What runs a group. 2026-10-02: with the compute unit its kernels run on
287/// (`venn::compute`), or `undeclared` for a kernel no family lists.
288fn kernels_of(g: &crate::fuser::Group, families: &crate::venn::families::Families) -> String {
289    if g.kernels.is_empty() {
290        format!("({} emitter) rule={}", g.emitter, g.rule)
291    } else {
292        let k: Vec<String> = g.kernels.iter().map(|k| k.to_string()).collect();
293        let unit = super::tc_policy::unit_tag(families, &g.kernels).unwrap_or_default();
294        format!("{} rule={} compute={unit}", k.join(" + "), g.rule)
295    }
296}