metrale_circuit/
fuser.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-28: The fuser: cover a circuit's nodes with rule groups for one mode and row count.
4//!
5//! Greedy and deterministic. Rules are tried by priority (highest first, then id), and each
6//! rule scans the nodes in execution order; a node joins at most one group. A rule is skipped
7//! when its mode or rows do not apply, a kernel is absent, a capability is missing, a policy
8//! setting it names differs, or its `differs` lever is not opted in.
9//!
10//! Owner: metrale-circuit.
11//! Invariants:
12//! - Every node of the mode's section ends in exactly one group, or `fuse` returns
13//!   [`FuseError::Uncovered`]; no node is left to an implicit kernel.
14//! - A policy must state every setting any rule reads ([`FuseError::PolicyMissing`]).
15//! - The plan is a pure function of its inputs; [`FusionPlan::digest`] changes exactly when
16//!   what the plan runs changes (see [`crate::digest`]).
17
18use std::collections::{BTreeMap, BTreeSet};
19
20use crate::format::Format;
21use crate::ir::{Circuit, EdgeIdx, NodeIdx, Section};
22use crate::rules::{KernelId, Mode, Numerics, Repeat, Rule};
23
24#[path = "fuser_match.rs"]
25mod fuser_match;
26
27pub(crate) use fuser_match::fits as pattern_fits;
28
29/// 2026-09-28: The kernels and capabilities a target provides (the boot probe's answer).
30#[derive(Debug, Clone, Default, PartialEq, Eq)]
31pub struct AvailableKernels {
32    /// 2026-09-28: Present entry points.
33    pub kernels: BTreeSet<KernelId>,
34    /// 2026-09-28: Present capability bits (`KernelCaps`).
35    pub caps: BTreeSet<String>,
36}
37
38impl AvailableKernels {
39    /// 2026-09-28: Every kernel and capability any of `rules` names: the offline view used
40    /// when no target is probed (`met circuit show`).
41    pub fn all_named_by(rules: &[Rule]) -> Self {
42        AvailableKernels {
43            kernels: rules
44                .iter()
45                .flat_map(|r| r.kernels.iter().cloned())
46                .collect(),
47            caps: rules
48                .iter()
49                .flat_map(|r| r.requires.iter().cloned())
50                .collect(),
51        }
52    }
53}
54
55/// 2026-09-28: What the recipe decides: opt-in levers for `differs` rules, and the settings
56/// (`kv_cache_dtype`, `row_tiers`, ...) that `when` clauses read.
57#[derive(Debug, Clone, Default, PartialEq, Eq)]
58pub struct Policy {
59    /// 2026-09-28: Levers whose `differs` rules may be selected.
60    pub opt_in_levers: BTreeSet<String>,
61    /// 2026-09-28: Settings, all stated explicitly.
62    pub settings: BTreeMap<String, String>,
63}
64
65/// 2026-09-28: Whether an edge is written to memory or stays inside one kernel.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
67pub enum EdgeState {
68    /// 2026-09-28: Written to a buffer between launches.
69    Materialized,
70    /// 2026-09-28: Produced and consumed inside this group only.
71    Fused(usize),
72}
73
74/// 2026-09-28: One launch group.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct Group {
77    /// 2026-09-28: The rule that formed it.
78    pub rule: String,
79    /// 2026-09-28: The kernels its emitter launches.
80    pub kernels: Vec<KernelId>,
81    /// 2026-09-28: Launch repetition.
82    pub repeat: Repeat,
83    /// 2026-09-29: Copy-engine transfers besides the kernels, and how often.
84    pub copies: Option<Repeat>,
85    /// 2026-09-28: Emitter id.
86    pub emitter: String,
87    /// 2026-09-28: Numerics class of the rule.
88    pub numerics: Numerics,
89    /// 2026-09-28: Member nodes, in pattern order.
90    pub nodes: Vec<NodeIdx>,
91    /// 2026-09-30: A `per_run` group's launches for each run of the plan's row table, in batch
92    /// order; empty for any other repeat.
93    pub runs: Vec<crate::runs::RunLaunches>,
94}
95
96impl Group {
97    /// 2026-09-30: Kernel launches per step at `rows` rows (per run for a `per_run` group).
98    pub fn launch_count(&self, rows: u64) -> u64 {
99        match self.repeat.count(rows) {
100            Some(c) => self.kernels.len() as u64 * c,
101            None => self.runs.iter().map(|r| r.launch_count()).sum(),
102        }
103    }
104
105    /// 2026-09-30: Copy-engine transfers per step at `rows` rows.
106    pub fn copy_count(&self, rows: u64) -> u64 {
107        match self.repeat {
108            crate::rules::Repeat::PerRun => self.runs.iter().map(|r| r.copy_count()).sum(),
109            _ => self.copies.and_then(|c| c.count(rows)).unwrap_or(0),
110        }
111    }
112}
113
114/// 2026-09-28: The fused plan of one circuit for one mode and row count.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct FusionPlan {
117    /// 2026-09-28: The circuit's arch.
118    pub arch: String,
119    /// 2026-09-28: Mode.
120    pub mode: Mode,
121    /// 2026-09-28: Padded rows the plan was chosen for.
122    pub rows: u64,
123    /// 2026-09-30: The row table of a [`Mode::VerifyBatch`] plan; `None` in every other mode.
124    pub table: Option<crate::runs::RowTable>,
125    /// 2026-09-28: Groups in execution order.
126    pub groups: Vec<Group>,
127    /// 2026-09-28: Per edge: `None` outside this mode's section, else its state.
128    pub edge_states: Vec<Option<EdgeState>>,
129    /// 2026-09-28: Per edge: the format it is stored in under this plan.
130    pub edge_formats: Vec<Format>,
131    /// 2026-09-28: Lower-case hex SHA-256 (see [`crate::digest`]).
132    pub digest: String,
133}
134
135impl FusionPlan {
136    /// 2026-09-28: Kernel launches per step: kernels times repetitions, over every group.
137    pub fn launches(&self) -> u64 {
138        self.groups.iter().map(|g| g.launch_count(self.rows)).sum()
139    }
140
141    /// 2026-09-29: Copy-engine transfers per step, over every group.
142    pub fn copies(&self) -> u64 {
143        self.groups.iter().map(|g| g.copy_count(self.rows)).sum()
144    }
145}
146
147/// 2026-09-28: Why no plan was produced.
148#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
149pub enum FuseError {
150    /// 2026-09-28: No applicable rule covers a node.
151    #[error("no applicable rule covers node `{node}` ({op}) in {mode} at {rows} rows")]
152    Uncovered {
153        /// 2026-09-28: Node id.
154        node: String,
155        /// 2026-09-28: Its op.
156        op: String,
157        /// 2026-09-28: Mode.
158        mode: &'static str,
159        /// 2026-09-28: Rows.
160        rows: u64,
161    },
162    /// 2026-09-28: A rule reads a setting the policy does not state.
163    #[error("rule `{rule}` reads setting `{key}`, which the policy does not state")]
164    PolicyMissing {
165        /// 2026-09-28: Rule id.
166        rule: String,
167        /// 2026-09-28: Setting.
168        key: String,
169    },
170    /// 2026-09-28: A kernel reads an edge in a format its producer does not store.
171    #[error("edge `{edge}` is stored as {stored} but rule `{rule}` reads it as {expected}")]
172    FormatConflict {
173        /// 2026-09-28: Edge id.
174        edge: String,
175        /// 2026-09-28: Stored format.
176        stored: String,
177        /// 2026-09-28: Reading rule.
178        rule: String,
179        /// 2026-09-28: Format it reads.
180        expected: String,
181    },
182    /// 2026-09-28: The mode's section has no nodes.
183    #[error("the circuit has no {0} nodes")]
184    EmptySection(&'static str),
185    /// 2026-09-28: The groups cannot be ordered (a rule set that fuses across a dependency).
186    #[error("the {0} groups formed do not have an execution order")]
187    Cycle(usize),
188    /// 2026-09-28: `rows` is zero.
189    #[error("a plan needs at least one row")]
190    ZeroRows,
191    /// 2026-09-30: A `verify_batch` plan without a row table, or a table in another mode.
192    #[error("{0}")]
193    RowTable(String),
194}
195
196/// 2026-09-28: The section a mode runs.
197pub fn section_of(mode: Mode) -> Section {
198    match mode {
199        Mode::Draft => Section::Draft,
200        Mode::Decode | Mode::MultiSeq | Mode::Verify | Mode::VerifyBatch => Section::Main,
201    }
202}
203
204/// 2026-09-28: Fuse `circuit` for `mode` at `rows` padded rows. A `verify_batch` plan needs a
205/// row table: [`fuse_table`].
206pub fn fuse(
207    circuit: &Circuit,
208    rules: &[Rule],
209    available: &AvailableKernels,
210    policy: &Policy,
211    mode: Mode,
212    rows: u64,
213) -> Result<FusionPlan, FuseError> {
214    if mode == Mode::VerifyBatch {
215        return Err(FuseError::RowTable(
216            "a verify_batch plan is fused for a row table (`fuse_table`)".into(),
217        ));
218    }
219    fuse_inner(circuit, rules, available, policy, (mode, rows, None))
220}
221
222/// 2026-09-30: Fuse `circuit` for a batched verify of `table`.
223pub fn fuse_table(
224    circuit: &Circuit,
225    rules: &[Rule],
226    available: &AvailableKernels,
227    policy: &Policy,
228    table: &crate::runs::RowTable,
229) -> Result<FusionPlan, FuseError> {
230    let rows = table.rows();
231    fuse_inner(
232        circuit,
233        rules,
234        available,
235        policy,
236        (Mode::VerifyBatch, rows, Some(table)),
237    )
238}
239
240fn fuse_inner(
241    circuit: &Circuit,
242    rules: &[Rule],
243    available: &AvailableKernels,
244    policy: &Policy,
245    (mode, rows, table): (Mode, u64, Option<&crate::runs::RowTable>),
246) -> Result<FusionPlan, FuseError> {
247    if rows == 0 {
248        return Err(FuseError::ZeroRows);
249    }
250    for r in rules {
251        if let Some(key) = r.when.keys().find(|k| !policy.settings.contains_key(*k)) {
252            return Err(FuseError::PolicyMissing {
253                rule: r.id.clone(),
254                key: key.clone(),
255            });
256        }
257    }
258    let section = section_of(mode);
259    let block_of = block_index(circuit);
260    let in_scope: Vec<bool> = block_of
261        .iter()
262        .map(|&b| circuit.blocks[b].section == section)
263        .collect();
264    if !in_scope.iter().any(|&s| s) {
265        return Err(FuseError::EmptySection(mode.name()));
266    }
267    let mut order: Vec<&Rule> = rules
268        .iter()
269        .filter(|r| applies(r, available, policy, mode, rows))
270        .filter(|r| {
271            r.runs.is_empty()
272                || table.is_some_and(|t| crate::runs::resolve_runs(t, &r.runs).is_some())
273        })
274        .collect();
275    order.sort_by(|a, b| b.priority.cmp(&a.priority).then_with(|| a.id.cmp(&b.id)));
276
277    let mut owner: Vec<Option<usize>> = vec![None; circuit.nodes.len()];
278    let mut found: Vec<(&Rule, Vec<NodeIdx>)> = Vec::new();
279    let m = fuser_match::Matcher {
280        circuit,
281        in_scope: &in_scope,
282        block_of: &block_of,
283    };
284    for rule in order {
285        for s in 0..circuit.nodes.len() {
286            if !in_scope[s] || owner[s].is_some() {
287                continue;
288            }
289            if let Some(chain) = m.chain_at(rule, s, &owner) {
290                for &n in &chain {
291                    owner[n] = Some(found.len());
292                }
293                found.push((rule, chain));
294            }
295        }
296    }
297    if let Some(n) = (0..circuit.nodes.len()).find(|&n| in_scope[n] && owner[n].is_none()) {
298        return Err(FuseError::Uncovered {
299            node: circuit.nodes[n].id.clone(),
300            op: circuit.nodes[n].op.name(),
301            mode: mode.name(),
302            rows,
303        });
304    }
305    let exec = fuser_match::execution_order(circuit, &in_scope, &owner, found.len());
306    if exec.len() != found.len() {
307        return Err(FuseError::Cycle(found.len()));
308    }
309    let mut remap = vec![0usize; found.len()];
310    for (new, &old) in exec.iter().enumerate() {
311        remap[old] = new;
312    }
313    let groups: Vec<Group> = exec
314        .iter()
315        .map(|&g| {
316            let (rule, nodes) = &found[g];
317            Group {
318                rule: rule.id.clone(),
319                kernels: rule.kernels.clone(),
320                repeat: rule.repeat,
321                copies: rule.copies,
322                emitter: rule.emitter.clone(),
323                numerics: rule.numerics.clone(),
324                nodes: nodes.clone(),
325                runs: table
326                    .and_then(|t| crate::runs::resolve_runs(t, &rule.runs))
327                    .filter(|_| !rule.runs.is_empty())
328                    .unwrap_or_default(),
329            }
330        })
331        .collect();
332    let owner: Vec<Option<usize>> = owner.iter().map(|o| o.map(|g| remap[g])).collect();
333    let edge_formats = stored_formats(circuit, &groups, rules);
334    check_reads(circuit, &groups, rules, &edge_formats)?;
335    // 2026-09-30: Outputs a pattern element marks `stored` stay in memory.
336    let stored: BTreeSet<EdgeIdx> = found
337        .iter()
338        .flat_map(|(rule, nodes)| {
339            rule.pattern
340                .iter()
341                .zip(nodes)
342                .filter(|(p, _)| p.stored)
343                .flat_map(|(_, &n)| circuit.nodes[n].outputs.iter().copied())
344        })
345        .collect();
346    let edge_states = edge_states(circuit, &in_scope, &owner, &stored);
347    let mut plan = FusionPlan {
348        arch: circuit.arch.clone(),
349        mode,
350        rows,
351        table: table.cloned(),
352        groups,
353        edge_states,
354        edge_formats,
355        digest: String::new(),
356    };
357    plan.digest = crate::digest::plan_digest(circuit, &plan);
358    Ok(plan)
359}
360
361fn applies(r: &Rule, avail: &AvailableKernels, policy: &Policy, mode: Mode, rows: u64) -> bool {
362    r.modes.contains(&mode)
363        && (r.rows.0..=r.rows.1).contains(&rows)
364        && r.kernels.iter().all(|k| avail.kernels.contains(k))
365        && r.requires.iter().all(|c| avail.caps.contains(c))
366        && r.when
367            .iter()
368            .all(|(k, v)| policy.settings.get(k) == Some(v))
369        && match &r.numerics {
370            Numerics::Differs { lever } => policy.opt_in_levers.contains(lever),
371            Numerics::BitIdentical { .. } | Numerics::Reference => true,
372        }
373}
374
375fn block_index(circuit: &Circuit) -> Vec<usize> {
376    let mut out = vec![0; circuit.nodes.len()];
377    for (b, blk) in circuit.blocks.iter().enumerate() {
378        for slot in &mut out[blk.first..blk.end] {
379            *slot = b;
380        }
381    }
382    out
383}
384
385fn rule_of<'a>(rules: &'a [Rule], id: &str) -> Option<&'a Rule> {
386    rules.iter().find(|r| r.id == id)
387}
388
389fn stored_formats(circuit: &Circuit, groups: &[Group], rules: &[Rule]) -> Vec<Format> {
390    let mut out: Vec<Format> = circuit.edges.iter().map(|e| e.format).collect();
391    for g in groups {
392        let Some(rule) = rule_of(rules, &g.rule) else {
393            continue;
394        };
395        for (p, &n) in rule.pattern.iter().zip(&g.nodes) {
396            if let Some(f) = p.writes {
397                for &e in &circuit.nodes[n].outputs {
398                    out[e] = f;
399                }
400            }
401        }
402    }
403    out
404}
405
406fn check_reads(
407    circuit: &Circuit,
408    groups: &[Group],
409    rules: &[Rule],
410    stored: &[Format],
411) -> Result<(), FuseError> {
412    for g in groups {
413        let Some(rule) = rule_of(rules, &g.rule) else {
414            continue;
415        };
416        for (p, &n) in rule.pattern.iter().zip(&g.nodes) {
417            let (Some(want), Some(&e)) = (p.input, circuit.nodes[n].inputs.first()) else {
418                continue;
419            };
420            if stored[e] != want {
421                return Err(FuseError::FormatConflict {
422                    edge: circuit.edges[e].id.clone(),
423                    stored: stored[e].name(),
424                    rule: rule.id.clone(),
425                    expected: want.name(),
426                });
427            }
428        }
429    }
430    Ok(())
431}
432
433fn edge_states(
434    circuit: &Circuit,
435    in_scope: &[bool],
436    owner: &[Option<usize>],
437    stored: &BTreeSet<EdgeIdx>,
438) -> Vec<Option<EdgeState>> {
439    circuit
440        .edges
441        .iter()
442        .enumerate()
443        .map(|(i, e)| {
444            let producer_in = e.producer.is_some_and(|p| in_scope[p]);
445            let read_in = e.consumers.iter().any(|&c| in_scope[c]);
446            if !producer_in && !read_in {
447                return None;
448            }
449            let g = e.producer.filter(|&p| in_scope[p]).and_then(|p| owner[p]);
450            let internal = g.is_some()
451                && !e.is_output
452                && !stored.contains(&i)
453                && !e.consumers.is_empty()
454                && e.consumers.iter().all(|&c| owner[c] == g);
455            Some(match (internal, g) {
456                (true, Some(g)) => EdgeState::Fused(g),
457                _ => EdgeState::Materialized,
458            })
459        })
460        .collect()
461}
462
463/// 2026-09-28: The edges a group reads from outside itself and the ones it writes, in edge
464/// order. Used by the renderers and the buffer planner.
465pub fn group_io(circuit: &Circuit, plan: &FusionPlan, g: usize) -> (Vec<EdgeIdx>, Vec<EdgeIdx>) {
466    let members: BTreeSet<NodeIdx> = plan.groups[g].nodes.iter().copied().collect();
467    let mut ins = BTreeSet::new();
468    let mut outs = BTreeSet::new();
469    for &n in &members {
470        let node = &circuit.nodes[n];
471        for &e in &node.inputs {
472            if circuit.edges[e]
473                .producer
474                .is_none_or(|p| !members.contains(&p))
475            {
476                ins.insert(e);
477            }
478        }
479        for &e in &node.outputs {
480            if plan.edge_states[e] == Some(EdgeState::Materialized) {
481                outs.insert(e);
482            }
483        }
484    }
485    (ins.into_iter().collect(), outs.into_iter().collect())
486}
487
488#[cfg(test)]
489#[path = "fuser_tests.rs"]
490mod fuser_tests;