metrale_circuit/rules.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-28: Fusion rules: `kernels/<hw>/common/FUSIONS.toml`. Each rule names an op chain,
4//! the kernel that runs it, where it applies (rows, modes, caps, policy settings) and how its
5//! numerics relate to the reference.
6//!
7//! Owner: metrale-circuit.
8//! Invariants:
9//! - A `bit_identical` rule names its microtest, and a `differs` rule its opt-in lever; a
10//! rule missing either is a load error, as is a lever or microtest on a `reference` rule.
11//! - Rule ids are unique; `rows` is a non-empty inclusive range; `modes` is non-empty.
12//! - Every rule cites the dispatch site that makes today's choice (`cite`).
13
14use std::collections::{BTreeMap, BTreeSet};
15
16use crate::format::Format;
17use crate::ir::{LayerKind, LinearRole, OpKind};
18
19/// 2026-09-28: Which forward a plan is for.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub enum Mode {
22 /// 2026-09-28: One sequence, one row.
23 Decode,
24 /// 2026-09-28: `n` sequences, one row each, padded to the batch ladder.
25 MultiSeq,
26 /// 2026-09-28: `K` draft rows of one sequence (MTP verify).
27 Verify,
28 /// 2026-09-28: The MTP draft head proposing one token for each of `n` sequences.
29 Draft,
30 /// 2026-09-30: `n` sequences' MTP verifies in one forward (`decode_verify_batched`):
31 /// `R = Σ k` rows, seq-major, planned for a row table ([`crate::runs::RowTable`]).
32 VerifyBatch,
33}
34
35impl Mode {
36 /// 2026-09-28: Every mode, in plan-file order.
37 /// 2026-09-30: [`Mode::VerifyBatch`] is not among them: its plans are keyed by a row table,
38 /// not a row count.
39 pub const ALL: [Mode; 4] = [Mode::Decode, Mode::MultiSeq, Mode::Verify, Mode::Draft];
40
41 /// 2026-09-28: The spelling in rules, file names and the CLI.
42 pub fn name(self) -> &'static str {
43 match self {
44 Mode::Decode => "decode",
45 Mode::MultiSeq => "multi_seq",
46 Mode::Verify => "verify",
47 Mode::Draft => "draft",
48 Mode::VerifyBatch => "verify_batch",
49 }
50 }
51
52 /// 2026-09-28: Parse [`Mode::name`].
53 pub fn parse(s: &str) -> Option<Self> {
54 Mode::ALL
55 .into_iter()
56 .chain([Mode::VerifyBatch])
57 .find(|m| m.name() == s)
58 }
59}
60
61/// 2026-09-28: A kernel entry point as the engine looks it up: the module name (the source
62/// stem after KERNEL.toml `[modules]` renames, e.g. `norm` for `rms_norm.cu`) and the
63/// function.
64#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
65pub struct KernelId {
66 /// 2026-09-28: Module, e.g. `norm`.
67 pub module: String,
68 /// 2026-09-28: Function, e.g. `residual_add_rms_norm`.
69 pub func: String,
70}
71
72impl std::fmt::Display for KernelId {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 write!(f, "{}::{}", self.module, self.func)
75 }
76}
77
78/// 2026-09-28: How a rule's output relates to the reference numerics.
79#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
80pub enum Numerics {
81 /// 2026-09-28: Byte-identical to the unfused chain, proven by the named microtest.
82 BitIdentical {
83 /// 2026-09-28: The microtest.
84 microtest: String,
85 },
86 /// 2026-09-28: Today's default: the reference definition, grandfathered.
87 Reference,
88 /// 2026-09-28: Changes numerics; selected only when `lever` is opted in.
89 Differs {
90 /// 2026-09-28: The opt-in lever.
91 lever: String,
92 },
93}
94
95impl Numerics {
96 /// 2026-09-28: `bit_identical`, `reference` or `differs`.
97 pub fn class(&self) -> &'static str {
98 match self {
99 Numerics::BitIdentical { .. } => "bit_identical",
100 Numerics::Reference => "reference",
101 Numerics::Differs { .. } => "differs",
102 }
103 }
104}
105
106/// 2026-09-28: One element of a rule's op chain.
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct PatternOp {
109 /// 2026-09-28: The op the node must be. For `linear` with a `roles` list, any of
110 /// [`PatternOp::roles`] matches and this holds the first.
111 pub op: OpKind,
112 /// 2026-09-28: Linear roles any of which matches; empty means `op` exactly.
113 pub roles: BTreeSet<LinearRole>,
114 /// 2026-09-28: The node's layer must be of this kind, when set.
115 pub layer_kind: Option<LayerKind>,
116 /// 2026-09-28: The node's template-local id must be this, when set.
117 pub local: Option<String>,
118 /// 2026-09-28: The node's weight format must be this, when set.
119 pub weight: Option<Format>,
120 /// 2026-09-28: The format the kernel reads its first input as. Checked after fusion
121 /// against the format the producing group writes, so a rule set whose kernels disagree
122 /// on a materialised edge is refused.
123 pub input: Option<Format>,
124 /// 2026-09-28: The format the kernel stores this element's outputs in, when it is not
125 /// the circuit's declared format (the verify conv stores BF16 where decode keeps F32).
126 pub writes: Option<Format>,
127 /// 2026-09-28: The kernel writes this element's outputs even though a later element
128 /// reads them, so they may have readers outside the group.
129 pub keep: bool,
130 /// 2026-09-30: The kernel stores this element's outputs in memory, where a later launch of
131 /// the group reads them back (a per-run schedule's conv rows), so they are materialized even
132 /// when every reader is in the group.
133 pub stored: bool,
134 /// 2026-09-28: The element joins the group by sitting in the same block as the first
135 /// element rather than by reading an output of an earlier one (a shared expert beside
136 /// the routed experts).
137 pub sibling: bool,
138 /// 2026-10-02: The format the kernel hands this element's outputs to later members of the
139 /// group in, where it is not the format the plan stores (a sum kept in FP32 registers);
140 /// `None` hands them on as stored ([`crate::pipeline::require`]).
141 pub holds: Option<Format>,
142 /// 2026-10-02: Steps of this element's node that the routing runs at another value than
143 /// the reference requirement (an activation quantized inside the group, a sum rounded to
144 /// BF16), stated so the plan shows them and the kernel must declare them.
145 pub steps: BTreeMap<crate::pipeline::StepKind, crate::pipeline::Value>,
146}
147
148/// 2026-09-28: How many times a group's kernels launch per step.
149#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
150pub enum Repeat {
151 /// 2026-09-28: Once.
152 Once,
153 /// 2026-09-28: Once per row (a per-sequence loop).
154 PerRow,
155 /// 2026-09-28: Once per `n`-row chunk.
156 Chunk(u64),
157 /// 2026-09-29: Once per row but the last.
158 PerRowButLast,
159 /// 2026-09-30: Per run of a batched verify, as the rule's selectors say
160 /// ([`Rule::runs`], [`crate::fuser::Group::runs`]); not a function of the rows alone.
161 PerRun,
162}
163
164impl Repeat {
165 /// 2026-09-28: Launches of each kernel for `rows` rows. 2026-09-30: `None` for
166 /// [`Repeat::PerRun`], whose count is its runs' ([`crate::fuser::Group::launch_count`]).
167 pub fn count(self, rows: u64) -> Option<u64> {
168 match self {
169 Repeat::Once => Some(1),
170 Repeat::PerRow => Some(rows),
171 Repeat::Chunk(c) => Some(rows.div_ceil(c)),
172 Repeat::PerRowButLast => Some(rows.saturating_sub(1)),
173 Repeat::PerRun => None,
174 }
175 }
176
177 /// 2026-09-28: `once`, `per_row` or `chunk<n>`.
178 pub fn name(self) -> String {
179 match self {
180 Repeat::Once => "once".into(),
181 Repeat::PerRow => "per_row".into(),
182 Repeat::Chunk(c) => format!("chunk{c}"),
183 Repeat::PerRowButLast => "per_row_but_last".into(),
184 Repeat::PerRun => "per_run".into(),
185 }
186 }
187
188 fn parse(s: &str) -> Option<Self> {
189 match s {
190 "once" => Some(Repeat::Once),
191 "per_row" => Some(Repeat::PerRow),
192 "per_row_but_last" => Some(Repeat::PerRowButLast),
193 "per_run" => Some(Repeat::PerRun),
194 _ => s
195 .strip_prefix("chunk")?
196 .parse::<u64>()
197 .ok()
198 .filter(|&c| c > 0)
199 .map(Repeat::Chunk),
200 }
201 }
202}
203
204/// 2026-09-28: One fusion rule.
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct Rule {
207 /// 2026-09-28: Unique id.
208 pub id: String,
209 /// 2026-09-28: The op chain, each element reading an output of the one before.
210 pub pattern: Vec<PatternOp>,
211 /// 2026-09-28: The kernels the emitter launches for the chain, in order. Empty for a
212 /// chain that runs as a copy-engine transfer (the embedding row copy).
213 pub kernels: Vec<KernelId>,
214 /// 2026-09-28: How often the kernels launch per step.
215 pub repeat: Repeat,
216 /// 2026-09-29: Copy-engine transfers the emitter issues besides its kernels, and how
217 /// often; `None` for none (the verify conv's state snapshots are `per_row_but_last`).
218 pub copies: Option<Repeat>,
219 /// 2026-09-28: The executor's emitter for this kernel.
220 pub emitter: String,
221 /// 2026-09-28: Inclusive row range.
222 pub rows: (u64, u64),
223 /// 2026-09-28: Modes it applies in.
224 pub modes: BTreeSet<Mode>,
225 /// 2026-09-28: Kernel capability bits it needs.
226 pub requires: BTreeSet<String>,
227 /// 2026-09-28: Policy settings that must hold (`kv_cache_dtype = "bf16"`).
228 pub when: BTreeMap<String, String>,
229 /// 2026-09-28: Numerics class.
230 pub numerics: Numerics,
231 /// 2026-09-28: Higher wins.
232 pub priority: i64,
233 /// 2026-09-28: The dispatch site(s) that make this choice today.
234 pub cite: String,
235 /// 2026-09-30: A `per_run` rule's selectors (`[[rule.run]]`), in file order; empty for any
236 /// other repeat.
237 pub runs: Vec<crate::runs::RunSelect>,
238}
239
240/// 2026-09-28: Why FUSIONS.toml did not load.
241#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
242pub enum RuleError {
243 /// 2026-09-28: Not valid TOML or not the rules shape.
244 #[error("FUSIONS.toml: {0}")]
245 Parse(String),
246 /// 2026-09-28: Two rules share an id.
247 #[error("duplicate rule id `{0}`")]
248 DuplicateId(String),
249 /// 2026-09-30: A malformed `[[runtime]]` route ([`crate::runtime`]).
250 #[error("runtime `{route}`: {detail}")]
251 Runtime {
252 /// 2026-09-30: Route id.
253 route: String,
254 /// 2026-09-30: What was wrong.
255 detail: String,
256 },
257 /// 2026-09-28: A pattern op the vocabulary does not have.
258 #[error("rule `{rule}`: {detail}")]
259 Op {
260 /// 2026-09-28: Rule id.
261 rule: String,
262 /// 2026-09-28: What was wrong.
263 detail: String,
264 },
265 /// 2026-09-28: `bit_identical` without `microtest`.
266 #[error("rule `{0}` is bit_identical but names no microtest")]
267 MissingMicrotest(String),
268 /// 2026-09-28: `differs` without `lever`.
269 #[error("rule `{0}` is differs but names no opt-in lever")]
270 MissingLever(String),
271 /// 2026-09-28: A `microtest` or `lever` on a class that takes none, or an unknown class.
272 #[error("rule `{rule}`: {detail}")]
273 Numerics {
274 /// 2026-09-28: Rule id.
275 rule: String,
276 /// 2026-09-28: What was wrong.
277 detail: String,
278 },
279 /// 2026-09-28: Empty pattern, empty modes, unknown mode, bad row range or empty cite.
280 #[error("rule `{rule}`: {detail}")]
281 Shape {
282 /// 2026-09-28: Rule id.
283 rule: String,
284 /// 2026-09-28: What was wrong.
285 detail: String,
286 },
287}
288
289#[path = "rules_parse.rs"]
290mod parse;
291pub(crate) use parse::parse_file;
292pub use parse::parse_rules;
293
294#[cfg(test)]
295#[path = "rules_tests.rs"]
296mod rules_tests;