metrale_circuit/pipeline/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-10-02: The numeric pipeline of a node: the formats it reads, the ordered steps inside it
4//! (activation and weight preparation, the multiply's operands, accumulation, scale application,
5//! element-wise compute, cache and state precision, routing and blending) each with its
6//! precision, and the formats it writes. One pipeline is REQUIRED per node of a plan
7//! ([`require`]: the circuit's formats under the serving policy) and one is DECLARED per kernel
8//! family, point or kernel in `KERNEL_FAMILIES.toml` ([`declare`]); [`check`] refuses a plan
9//! whose kernels declare anything but exactly what the plan requires.
10//!
11//! Owner: metrale-circuit (pipeline).
12//! Invariants:
13//! - The step vocabulary of every op is one table ([`vocab::steps_of`]); a pipeline names every
14//!   step of its op's vocabulary in that order, and nothing else.
15//! - Every value has one canonical spelling that parses back to itself (`Value::parse(k,
16//!   v.name()) == Ok(v)`), so the golden plans and the manifests read one text per value.
17//! - Nothing defaults: a step whose precision cannot be stated is an error, never a guess.
18//! - Precision is about values, not instructions: a BF16 activation held in an FP32 register is
19//!   BF16, an E4M3 weight widened exactly is the precision it widens to; which unit and atom
20//!   carry the multiply is the compute unit's (`venn::compute`).
21
22use std::fmt;
23
24use crate::format::Format;
25use crate::state::StateDtype;
26
27pub mod check;
28pub mod declare;
29pub mod query;
30pub mod require;
31pub mod vocab;
32
33#[path = "act_policy.rs"]
34mod act_policy;
35
36pub use check::{PlanPipelines, check_plan};
37pub use require::{Need, required};
38
39/// 2026-10-02: The element precision of a value inside a kernel (registers, operands, sums).
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub enum Num {
42    /// 2026-10-02: bfloat16.
43    Bf16,
44    /// 2026-10-02: IEEE half.
45    F16,
46    /// 2026-10-02: float32.
47    F32,
48    /// 2026-10-02: FP8 E4M3 values (an FP8 activation or weight as multiplied).
49    E4m3,
50    /// 2026-10-02: FP4 E2M1 values (an NVFP4 activation or weight as multiplied).
51    E2m1,
52}
53
54const NUMS: [(Num, &str); 5] = [
55    (Num::Bf16, "bf16"),
56    (Num::F16, "f16"),
57    (Num::F32, "f32"),
58    (Num::E4m3, "e4m3"),
59    (Num::E2m1, "e2m1"),
60];
61
62impl Num {
63    /// 2026-10-02: The canonical spelling.
64    pub fn name(self) -> &'static str {
65        NUMS.iter()
66            .find(|(n, _)| *n == self)
67            .map_or("?", |(_, s)| s)
68    }
69
70    /// 2026-10-02: Parse the canonical spelling.
71    pub fn parse(s: &str) -> Option<Self> {
72        NUMS.iter().find(|(_, n)| *n == s).map(|(v, _)| *v)
73    }
74
75    /// 2026-10-02: The precision of a tensor format's values: an FP8 tensor is E4M3, an NVFP4 one
76    /// E2M1; `None` for `i32`, which no multiply or compute step reads as a number.
77    pub fn of_format(f: Format) -> Option<Self> {
78        match f {
79            Format::Bf16 => Some(Num::Bf16),
80            Format::F32 => Some(Num::F32),
81            Format::Fp8E4m3 { .. } => Some(Num::E4m3),
82            Format::Nvfp4 { .. } => Some(Num::E2m1),
83            Format::I32 => None,
84        }
85    }
86}
87
88/// 2026-10-02: A step of a node's pipeline. Each has one value shape ([`vocab::shape_of`]).
89#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
90pub enum StepKind {
91    /// 2026-10-02: Rows gathered by expert id (or embedding rows by token id): their format.
92    Gather,
93    /// 2026-10-02: The format the activation is rounded or quantized to before the multiply.
94    Act,
95    /// 2026-10-02: The stored weight format and the precision of its unscaled values as multiplied.
96    Weight,
97    /// 2026-10-02: The multiply's operand precisions, activation first.
98    Mma,
99    /// 2026-10-02: The precision dot products (or attention's PV sums) accumulate in.
100    Accumulate,
101    /// 2026-10-02: The precision weight and activation scales are applied in; none without scales.
102    Scale,
103    /// 2026-10-02: The precision of an element-wise op's or a reduction's arithmetic.
104    Compute,
105    /// 2026-10-02: Data moved without arithmetic: its format.
106    Move,
107    /// 2026-10-02: The KV-cache dtype read or written.
108    Cache,
109    /// 2026-10-02: Attention: the precision of the QK dot products.
110    Scores,
111    /// 2026-10-02: Attention: the precision of the softmax.
112    Softmax,
113    /// 2026-10-02: The precision of the recurrent state as stored and updated.
114    State,
115    /// 2026-10-02: Routing: the precision of the scoring function and of the routing weights.
116    Score,
117    /// 2026-10-02: Blend: the precision of the weighted sum of each token's top-k expert rows.
118    Scatter,
119    /// 2026-10-02: Blend: the precision the (gated) shared expert is added in; none without one.
120    Combine,
121    /// 2026-10-02: A cross-rank reduction's precision.
122    Reduce,
123    /// 2026-10-02: Greedy selection: the format of the values compared.
124    Compare,
125}
126
127/// 2026-10-02: A step's value.
128#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
129pub enum Value {
130    /// 2026-10-02: A tensor format (`gather`, `act`, `move`, `compare`).
131    Format(Format),
132    /// 2026-10-02: `<stored>-><operand>` (`weight`).
133    Weight {
134        /// 2026-10-02: The weight as loaded.
135        stored: Format,
136        /// 2026-10-02: Its unscaled values' precision as multiplied.
137        operand: Num,
138    },
139    /// 2026-10-02: `<a>*<b>` (`mma`).
140    Mma {
141        /// 2026-10-02: The activation operand.
142        a: Num,
143        /// 2026-10-02: The weight operand.
144        b: Num,
145    },
146    /// 2026-10-02: A precision.
147    Num(Num),
148    /// 2026-10-02: A precision, or `none` where the step does not apply (no scales, no shared
149    /// expert).
150    OptNum(Option<Num>),
151    /// 2026-10-02: A KV-cache dtype, spelled as `--kv-cache-dtype` spells it.
152    Kv(String),
153    /// 2026-10-02: A recurrent state's storage element.
154    State(StateDtype),
155}
156
157/// 2026-10-02: The spelling of a state dtype (the circuit TOML's, [`StateDtype::name`]).
158pub fn state_name(d: StateDtype) -> &'static str {
159    d.name()
160}
161
162impl Value {
163    /// 2026-10-02: The canonical spelling.
164    pub fn name(&self) -> String {
165        match self {
166            Value::Format(f) => f.name(),
167            Value::Weight { stored, operand } => format!("{}->{}", stored.name(), operand.name()),
168            Value::Mma { a, b } => format!("{}*{}", a.name(), b.name()),
169            Value::Num(n) => n.name().to_string(),
170            Value::OptNum(None) => "none".to_string(),
171            Value::OptNum(Some(n)) => n.name().to_string(),
172            Value::Kv(k) => k.clone(),
173            Value::State(d) => state_name(*d).to_string(),
174        }
175    }
176}
177
178/// 2026-10-02: One step and its value.
179#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
180pub struct Step {
181    /// 2026-10-02: Which step.
182    pub kind: StepKind,
183    /// 2026-10-02: Its value.
184    pub value: Value,
185}
186
187/// 2026-10-02: A node's pipeline: input formats in the node's input order, the op's steps in
188/// vocabulary order, output formats in the node's output order.
189#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
190pub struct NodePipeline {
191    /// 2026-10-02: The format each input is read in.
192    pub inputs: Vec<Format>,
193    /// 2026-10-02: The steps.
194    pub steps: Vec<Step>,
195    /// 2026-10-02: The format each output is written in, or handed on in inside its group.
196    pub outputs: Vec<Format>,
197}
198
199/// 2026-10-02: The parts of a pipeline a plan's rule states rather than the reference: steps,
200/// and the inputs and outputs whose in-group hand-off format it states (`holds`). Rendered
201/// ` (rule)`.
202#[derive(Debug, Clone, Default, PartialEq, Eq)]
203pub struct Stated {
204    /// 2026-10-02: Stated steps.
205    pub steps: std::collections::BTreeSet<StepKind>,
206    /// 2026-10-02: Indices of inputs read in a stated hand-off format.
207    pub inputs: std::collections::BTreeSet<usize>,
208    /// 2026-10-02: Indices of outputs handed on in a stated format.
209    pub outputs: std::collections::BTreeSet<usize>,
210}
211
212impl NodePipeline {
213    /// 2026-10-02: The pipeline as facts: `in <formats>`, `<step> <value>` per step, `out
214    /// <formats>`, each part a rule states followed by ` (rule)`; `-` for no inputs or outputs.
215    pub fn facts(&self, stated: &Stated) -> Vec<String> {
216        let mark = |on: bool| if on { " (rule)" } else { "" };
217        let list = |v: &[Format], marked: &dyn Fn(usize) -> bool| {
218            if v.is_empty() {
219                "-".to_string()
220            } else {
221                v.iter()
222                    .enumerate()
223                    .map(|(i, f)| format!("{}{}", f.name(), mark(marked(i))))
224                    .collect::<Vec<_>>()
225                    .join(",")
226            }
227        };
228        let mut out = vec![format!(
229            "in {}",
230            list(&self.inputs, &|i| stated.inputs.contains(&i))
231        )];
232        out.extend(self.steps.iter().map(|s| {
233            format!(
234                "{} {}{}",
235                vocab::step_name(s.kind),
236                s.value.name(),
237                mark(stated.steps.contains(&s.kind))
238            )
239        }));
240        out.push(format!(
241            "out {}",
242            list(&self.outputs, &|i| stated.outputs.contains(&i))
243        ));
244        out
245    }
246
247    /// 2026-10-02: `in -> [step value | ...] -> out`, ASCII ([`NodePipeline::facts`]).
248    pub fn render(&self, stated: &Stated) -> String {
249        let facts = self.facts(stated);
250        let (first, rest) = facts.split_first().map_or(("", &[][..]), |(f, r)| (f, r));
251        let (last, steps) = rest.split_last().map_or(("", &[][..]), |(l, s)| (l, s));
252        format!(
253            "{} -> [{}] -> {}",
254            first.trim_start_matches("in "),
255            steps.join(" | "),
256            last.trim_start_matches("out ")
257        )
258    }
259}
260
261impl fmt::Display for NodePipeline {
262    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263        f.write_str(&self.render(&Default::default()))
264    }
265}
266
267/// 2026-10-02: What differs between a required and a declared pipeline, one entry per field
268/// (`in[1]`, `weight`, `out[0]`), as `field: required X, declared Y`.
269pub fn differences(required: &NodePipeline, declared: &NodePipeline) -> Vec<String> {
270    let mut out = Vec::new();
271    let fmts = |label: &str, r: &[Format], d: &[Format], out: &mut Vec<String>| {
272        if r.len() != d.len() {
273            out.push(format!(
274                "{label}: required {} formats, declared {}",
275                r.len(),
276                d.len()
277            ));
278            return;
279        }
280        for (i, (a, b)) in r.iter().zip(d).enumerate() {
281            if a != b {
282                out.push(format!("{label}[{i}]: required {a}, declared {b}"));
283            }
284        }
285    };
286    fmts("in", &required.inputs, &declared.inputs, &mut out);
287    for (r, d) in required.steps.iter().zip(&declared.steps) {
288        if r != d {
289            out.push(format!(
290                "{}: required {}, declared {}",
291                vocab::step_name(r.kind),
292                r.value.name(),
293                d.value.name()
294            ));
295        }
296    }
297    if required.steps.len() != declared.steps.len() {
298        out.push("steps: the two pipelines name different step lists".into());
299    }
300    fmts("out", &required.outputs, &declared.outputs, &mut out);
301    out
302}
303
304/// 2026-10-02: Why a pipeline could not be required, declared or matched.
305#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
306pub enum PipelineError {
307    /// 2026-10-02: The policy or the circuit does not state what a step needs.
308    #[error("node `{node}` ({op}): no required pipeline: {detail}")]
309    Required {
310        /// 2026-10-02: Node id.
311        node: String,
312        /// 2026-10-02: Its op.
313        op: String,
314        /// 2026-10-02: What is missing or contradictory.
315        detail: String,
316    },
317    /// 2026-10-02: A group's kernels are in no family, so their pipeline is undeclared.
318    #[error("node `{node}` ({op}) runs {kernels}, which no kernel family declares a pipeline for")]
319    Undeclared {
320        /// 2026-10-02: Node id.
321        node: String,
322        /// 2026-10-02: Its op.
323        op: String,
324        /// 2026-10-02: The group's kernels or emitter.
325        kernels: String,
326    },
327    /// 2026-10-02: A family's declaration cannot be resolved for a node.
328    #[error("node `{node}` ({op}) in family `{family}`: {detail}")]
329    Declared {
330        /// 2026-10-02: Node id.
331        node: String,
332        /// 2026-10-02: Its op.
333        op: String,
334        /// 2026-10-02: Family id.
335        family: String,
336        /// 2026-10-02: Why.
337        detail: String,
338    },
339    /// 2026-10-02: Kernels declare another pipeline than a plan requires, one entry per node.
340    #[error(
341        "{} node(s) run a kernel whose declared pipeline is not the required one:\n  {}",
342        .0.len(),
343        .0.iter().map(Mismatch::describe).collect::<Vec<_>>().join("\n  ")
344    )]
345    Mismatch(Vec<Mismatch>),
346}
347
348/// 2026-10-02: One node whose kernel declares another pipeline than its plan requires.
349#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
350pub struct Mismatch {
351    /// 2026-10-02: Node id.
352    pub node: String,
353    /// 2026-10-02: The rule of its group.
354    pub rule: String,
355    /// 2026-10-02: The family that declares the kernel's pipeline.
356    pub family: String,
357    /// 2026-10-02: What differs ([`differences`]).
358    pub diffs: Vec<String>,
359}
360
361impl Mismatch {
362    /// 2026-10-02: One line.
363    pub fn describe(&self) -> String {
364        format!(
365            "{} (rule `{}`, family `{}`): {}",
366            self.node,
367            self.rule,
368            self.family,
369            self.diffs.join("; ")
370        )
371    }
372}
373
374#[cfg(test)]
375#[path = "pipeline_tests.rs"]
376mod pipeline_tests;