1use 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
31pub struct AvailableKernels {
32 pub kernels: BTreeSet<KernelId>,
34 pub caps: BTreeSet<String>,
36}
37
38impl AvailableKernels {
39 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
58pub struct Policy {
59 pub opt_in_levers: BTreeSet<String>,
61 pub settings: BTreeMap<String, String>,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
67pub enum EdgeState {
68 Materialized,
70 Fused(usize),
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct Group {
77 pub rule: String,
79 pub kernels: Vec<KernelId>,
81 pub repeat: Repeat,
83 pub copies: Option<Repeat>,
85 pub emitter: String,
87 pub numerics: Numerics,
89 pub nodes: Vec<NodeIdx>,
91 pub runs: Vec<crate::runs::RunLaunches>,
94}
95
96impl Group {
97 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 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#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct FusionPlan {
117 pub arch: String,
119 pub mode: Mode,
121 pub rows: u64,
123 pub table: Option<crate::runs::RowTable>,
125 pub groups: Vec<Group>,
127 pub edge_states: Vec<Option<EdgeState>>,
129 pub edge_formats: Vec<Format>,
131 pub digest: String,
133}
134
135impl FusionPlan {
136 pub fn launches(&self) -> u64 {
138 self.groups.iter().map(|g| g.launch_count(self.rows)).sum()
139 }
140
141 pub fn copies(&self) -> u64 {
143 self.groups.iter().map(|g| g.copy_count(self.rows)).sum()
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
149pub enum FuseError {
150 #[error("no applicable rule covers node `{node}` ({op}) in {mode} at {rows} rows")]
152 Uncovered {
153 node: String,
155 op: String,
157 mode: &'static str,
159 rows: u64,
161 },
162 #[error("rule `{rule}` reads setting `{key}`, which the policy does not state")]
164 PolicyMissing {
165 rule: String,
167 key: String,
169 },
170 #[error("edge `{edge}` is stored as {stored} but rule `{rule}` reads it as {expected}")]
172 FormatConflict {
173 edge: String,
175 stored: String,
177 rule: String,
179 expected: String,
181 },
182 #[error("the circuit has no {0} nodes")]
184 EmptySection(&'static str),
185 #[error("the {0} groups formed do not have an execution order")]
187 Cycle(usize),
188 #[error("a plan needs at least one row")]
190 ZeroRows,
191 #[error("{0}")]
193 RowTable(String),
194}
195
196pub 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
204pub 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
222pub 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 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
463pub 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;