metrale_circuit/hardware/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-30: The hardware axis of the circuit (`met circuit plan --hardware <device>`): point a
4//! model and a target device at it and get the fused plan the device's kernel class can run,
5//! the gap report against that class's kernel set ranked by the device's roofline, decode and
6//! prefill estimates, and a memory-fit check.
7//!
8//! - [`device`]: `kernels/DEVICES.toml`, the SKU registry (roofline, native instruction kinds).
9//! - [`class`]: a class's chain, rules and families, inherited along `HARDWARE.toml` `inherits`.
10//! - [`avail`]: which kernels the device can run, read from the sources' guards.
11//! - [`exec`]: how a node's declared formats execute on the device (never a silent upcast).
12//! - [`plan`] / [`gaps`] / [`estimate`]: fuse with gaps marked, classify, estimate.
13//! - [`model`]: the model seam ([`model::CircuitSource`]).
14//!
15//! Owner: metrale-circuit (hardware).
16//! Invariants:
17//! - Pure: every file arrives through [`sources::KernelTree`] (SBIO); nothing reads the disk,
18//!   the environment or a clock.
19//! - Deterministic: the same inputs render the same report bytes (the `--check` tests rely on
20//!   it).
21
22pub mod avail;
23pub mod class;
24pub mod device;
25pub mod estimate;
26pub mod exec;
27pub mod gaps;
28pub mod model;
29pub mod model_checkpoint;
30pub mod plan;
31mod render;
32mod render_matrix;
33mod render_routes;
34pub mod sources;
35pub mod tc_policy;
36mod tc_policy_render;
37
38#[cfg(test)]
39mod fp4_costing_tests;
40#[cfg(test)]
41mod hardware_tests;
42#[cfg(test)]
43#[path = "runtime_tests.rs"]
44mod runtime_tests;
45#[cfg(test)]
46mod test_fixture;
47
48use std::collections::BTreeMap;
49
50use crate::fuser::Policy;
51use crate::render::Header;
52use crate::rules::KernelId;
53use crate::venn::Class;
54use crate::venn::roofline::CostError;
55
56pub use device::{Device, Registry, parse_devices};
57pub use model::{CircuitSource, InstancesSource, ModelSpec, ModelUnderPlan, PrecisionChoice};
58pub use model_checkpoint::CheckpointSource;
59pub use render::render_report;
60pub use render_matrix::{PortList, port_lists, summary_row};
61pub use render_routes::plan_text;
62pub use sources::{ClassSources, KernelTree, Module};
63
64/// 2026-09-30: Why a hardware plan could not be built.
65#[derive(Debug, Clone, PartialEq, thiserror::Error)]
66pub enum HwError {
67    /// 2026-09-30: `kernels/DEVICES.toml`.
68    #[error("DEVICES.toml: {0}")]
69    Registry(String),
70    /// 2026-09-30: A device id the registry does not list.
71    #[error("unknown device `{id}` (kernels/DEVICES.toml lists: {})", .known.join(", "))]
72    UnknownDevice {
73        /// 2026-09-30: The id asked for.
74        id: String,
75        /// 2026-09-30: The ids listed.
76        known: Vec<String>,
77    },
78    /// 2026-09-30: A class's HARDWARE.toml, FUSIONS.toml, KERNEL_FAMILIES.toml or sources.
79    #[error("{0}")]
80    Class(String),
81    /// 2026-09-30: The class's build flags and the device's instruction kinds disagree.
82    #[error(
83        "kernels/{class}/HARDWARE.toml {} -D{macro_name}, but device `{device}` {} {requires}: \
84         the build and the registry contradict each other",
85        if *.defined { "defines" } else { "does not define" },
86        if *.defined { "claims" } else { "lacks" }
87    )]
88    BuildContradictsDevice {
89        /// 2026-09-30: Device id.
90        device: String,
91        /// 2026-09-30: Class.
92        class: String,
93        /// 2026-09-30: Guard macro.
94        macro_name: String,
95        /// 2026-09-30: The instruction kind it stands for.
96        requires: String,
97        /// 2026-09-30: The class defines the macro.
98        defined: bool,
99    },
100    /// 2026-10-02: A plan runs a policy-covered op off tensor cores with no exemption.
101    #[error(
102        "kernels/{class}/HARDWARE.toml [tensor_core_policy]: these ops must run on tensor cores \
103         and no exemption lists them:\n  {violations}"
104    )]
105    TensorCore {
106        /// 2026-10-02: The class.
107        class: String,
108        /// 2026-10-02: One line per violation ([`tc_policy::describe`]).
109        violations: String,
110    },
111    /// 2026-09-30: The model.
112    #[error("{0}")]
113    Model(String),
114    /// 2026-09-30: Fusing.
115    #[error("{0}")]
116    Plan(String),
117    /// 2026-09-30: The roofline.
118    #[error(transparent)]
119    Cost(#[from] CostError),
120}
121
122/// 2026-09-30: A built hardware report.
123#[derive(Debug, Clone)]
124pub struct HwReport {
125    /// 2026-09-30: The model.
126    pub model: ModelUnderPlan,
127    /// 2026-09-30: The device's class, resolved.
128    pub resolved: plan::Resolved,
129    /// 2026-09-30: The policy planned with.
130    pub policy: Policy,
131    /// 2026-09-30: Settings the class's defaults changed from the model's source.
132    pub class_settings: BTreeMap<String, String>,
133    /// 2026-09-30: Plan header.
134    pub header: Header,
135    /// 2026-09-30: One table per report run ([`plan::report_runs`]).
136    pub tables: Vec<gaps::GapTable>,
137    /// 2026-10-02: The tensor-core audit of each report run (its routes included), in the
138    /// order of `tables`; empty when the class states no policy.
139    pub tensor_core: Vec<tc_policy::TcAudit>,
140    /// 2026-09-30: The runtime routes that apply to those runs, estimated.
141    pub routes: Vec<render_routes::RouteRow>,
142    /// 2026-09-30: Prefill estimates: (tokens, microseconds).
143    pub prefill: Vec<(u64, f64)>,
144    /// 2026-09-30: Weights, KV and state.
145    pub footprint: estimate::Footprint,
146    /// 2026-09-30: Weight bytes one decode row reads.
147    pub weight_floor: f64,
148    /// 2026-09-30: (declared pair, weight, activation, execution) to node count.
149    pub exec: BTreeMap<(String, String, String, exec::Exec), usize>,
150    /// 2026-09-30: Kernels of the rules this model's ops and policy could select that the
151    /// device cannot run.
152    pub absent: Vec<(KernelId, avail::Absence)>,
153    /// 2026-09-30: Every registry device's usable bytes, for the "does not fit" advice.
154    pub others: Vec<(String, f64)>,
155    /// 2026-09-30: The regenerating command.
156    pub command: String,
157}
158
159impl HwReport {
160    /// 2026-09-30: Share of the step at `table` covered by shared kernels (measured, or any).
161    pub fn covered(&self, table: usize, measured_only: bool) -> f64 {
162        let classes: &[Class] = if measured_only {
163            &[Class::Shared]
164        } else {
165            &[Class::Shared, Class::SharedUnmeasured]
166        };
167        self.tables.get(table).map_or(0.0, |t| t.share_of(classes))
168    }
169}
170
171/// 2026-09-30: One fused plan of `model` on `device_id` at `run`, with the policy and plan
172/// header it was made under: the plan `met circuit plan --format plan` prints and
173/// `met circuit display --hardware` draws.
174#[derive(Debug, Clone)]
175pub struct OnePlan {
176    /// 2026-09-30: The device's class, resolved.
177    pub resolved: plan::Resolved,
178    /// 2026-09-30: The policy after the class's defaults.
179    pub policy: Policy,
180    /// 2026-09-30: The plan header.
181    pub header: Header,
182    /// 2026-09-30: The plan.
183    pub planned: plan::Planned,
184}
185
186/// 2026-09-30: Plan `model` on `device_id` at `run`.
187pub fn plan_one(
188    registry: &Registry,
189    device_id: &str,
190    tree: &dyn KernelTree,
191    model: &ModelUnderPlan,
192    run: crate::venn::Run,
193) -> Result<OnePlan, HwError> {
194    let resolved = plan::resolve(registry, device_id, tree, model)?;
195    let (policy, _) = model::policy_on_class(
196        &model.policy,
197        model.settings_class.as_deref(),
198        &resolved.chain[0],
199    )?;
200    let header =
201        crate::render::with_settings(&model::header_on(model, &resolved.device.class), &policy);
202    let planned = plan::fuse_on(&resolved, &model.circuit, &policy, run)?;
203    tc_policy::enforce(&resolved, &model.circuit, &planned)?;
204    Ok(OnePlan {
205        resolved,
206        policy,
207        header,
208        planned,
209    })
210}
211
212/// 2026-09-30: Draw `one` as `met circuit display` does (placeholder groups show their
213/// `novel` emitter).
214pub fn display_on(
215    model: &ModelUnderPlan,
216    one: &OnePlan,
217    opts: &crate::display::DisplayOpts,
218) -> Result<crate::display::Document, crate::LoadError> {
219    let plan = &one.planned.plan;
220    let buffers = crate::planner::plan_buffers(&model.circuit, plan, plan.rows)?;
221    let info = crate::display::DisplayInfo {
222        checkpoint: model.checkpoint.clone(),
223        recipe: format!("{} on {}", model.label, one.resolved.device.id),
224        bytes: Some((buffers.materialized_bytes, buffers.arena_bytes)),
225        pipelines: (0..model.circuit.nodes.len())
226            .map(|n| one.planned.pipelines.facts(n))
227            .collect(),
228    };
229    Ok(crate::display::display(&model.circuit, plan, &info, opts)?)
230}
231
232/// 2026-09-30: Some node of `c` has the op (and, for a linear, a role) the rule's first pattern
233/// op names: the rule could apply to this model at all.
234fn heads_a_node(r: &crate::rules::Rule, c: &crate::ir::Circuit) -> bool {
235    r.pattern.first().is_some_and(|p| {
236        c.nodes.iter().any(|n| match n.op {
237            crate::ir::OpKind::Linear(role) if !p.roles.is_empty() => p.roles.contains(&role),
238            op => op == p.op,
239        })
240    })
241}
242
243/// 2026-09-30: Prompt lengths every report estimates.
244pub const PREFILL_TOKENS: [u64; 2] = [4096, 32768];
245
246/// 2026-09-30: Build the report of `model` on `device_id`.
247pub fn build_report(
248    registry: &Registry,
249    device_id: &str,
250    tree: &dyn KernelTree,
251    model: ModelUnderPlan,
252    command: String,
253) -> Result<HwReport, HwError> {
254    let resolved = plan::resolve(registry, device_id, tree, &model)?;
255    let (policy, class_settings) = model::policy_on_class(
256        &model.policy,
257        model.settings_class.as_deref(),
258        &resolved.chain[0],
259    )?;
260    let header =
261        crate::render::with_settings(&model::header_on(&model, &resolved.device.class), &policy);
262    let c = &model.circuit;
263    let mut tables = Vec::new();
264    let mut tensor_core = Vec::new();
265    for run in plan::report_runs() {
266        let planned = plan::fuse_on(&resolved, c, &policy, run)?;
267        tensor_core.extend(tc_policy::enforce(&resolved, c, &planned)?);
268        tables.push(gaps::gap_table(
269            &resolved,
270            c,
271            &policy.settings,
272            &model.label,
273            planned,
274        )?);
275    }
276    let routes = render_routes::route_rows(&resolved, c, &policy, &model.label, &tables)?;
277    let rf = |n| resolved.roofline_of(n);
278    let prefill = PREFILL_TOKENS
279        .iter()
280        .map(|&t| Ok((t, estimate::prefill_us(c, &policy.settings, &rf, t)?)))
281        .collect::<Result<Vec<_>, HwError>>()?;
282    let footprint = estimate::footprint(c, &policy.settings).map_err(HwError::Model)?;
283    let weight_floor = estimate::weight_floor_bytes(c).map_err(HwError::Model)?;
284    let mut exec = BTreeMap::new();
285    for (i, n) in c.nodes.iter().enumerate() {
286        let (Some(w), Some(a), Some(e)) =
287            (n.weight, estimate::activation_of(c, n), resolved.exec[i])
288        else {
289            continue;
290        };
291        *exec
292            .entry((exec::pair_name(w, a), w.name(), a.name(), e))
293            .or_insert(0) += 1;
294    }
295    let absent = resolved
296        .rule_list()
297        .iter()
298        .filter(|r| {
299            r.when
300                .iter()
301                .all(|(k, v)| policy.settings.get(k) == Some(v))
302                && heads_a_node(r, c)
303        })
304        .flat_map(|r| r.kernels.iter())
305        .filter_map(|k| {
306            resolved
307                .availability
308                .absent
309                .get(k)
310                .map(|a| (k.clone(), a.clone()))
311        })
312        .collect::<BTreeMap<_, _>>()
313        .into_iter()
314        .collect();
315    Ok(HwReport {
316        model,
317        resolved,
318        policy,
319        class_settings,
320        header,
321        tables,
322        tensor_core,
323        routes,
324        prefill,
325        footprint,
326        weight_floor,
327        exec,
328        absent,
329        others: registry
330            .devices
331            .iter()
332            .map(|d| (d.id.clone(), d.memory_bytes * d.usable_fraction))
333            .collect(),
334        command,
335    })
336}