1mod cse;
6mod dce;
7mod fold;
8mod rebuild;
9
10pub use cse::CommonSubexpressionElimination;
11pub use dce::DeadCodeElimination;
12pub use fold::{ConstEvaluator, ConstantFolding};
13
14use crate::{Error, Graph, Limits, TensorType};
15use std::sync::Arc;
16
17#[derive(Debug)]
19pub struct PassOutput {
20 pub graph: Graph,
21 pub changed: usize,
22}
23
24pub trait Pass {
25 fn name(&self) -> &'static str;
27 fn run(&self, graph: &Graph) -> Result<PassOutput, Error>;
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct PassReport {
32 pub pass: &'static str,
33 pub values_before: usize,
34 pub values_after: usize,
35 pub changed: usize,
36}
37
38#[derive(Debug)]
39pub struct Optimized {
40 pub graph: Graph,
41 pub reports: Vec<PassReport>,
42 pub stages: Vec<(String, String)>,
44}
45
46impl Optimized {
47 pub fn dump_text(&self) -> String {
48 self.stages
49 .iter()
50 .map(|(label, ir)| format!("== {label} ==\n{ir}\n"))
51 .collect()
52 }
53}
54
55pub struct PassManager {
56 passes: Vec<Box<dyn Pass>>,
57 limits: Limits,
58 dump: bool,
59}
60
61impl PassManager {
62 pub fn new(limits: Limits) -> Self {
63 Self {
64 passes: Vec::new(),
65 limits,
66 dump: false,
67 }
68 }
69
70 #[allow(clippy::should_implement_trait)]
71 pub fn add(mut self, pass: impl Pass + 'static) -> Self {
72 self.passes.push(Box::new(pass));
73 self
74 }
75
76 pub fn without(mut self, name: &str) -> Self {
78 self.passes.retain(|pass| pass.name() != name);
79 self
80 }
81
82 pub fn with_dumps(mut self, dump: bool) -> Self {
83 self.dump = dump;
84 self
85 }
86
87 pub fn pass_names(&self) -> Vec<&'static str> {
88 self.passes.iter().map(|pass| pass.name()).collect()
89 }
90
91 pub fn run(&self, graph: &Graph) -> Result<Optimized, Error> {
94 check(graph, &self.limits, "input graph is invalid")?;
95 let expected = interface(graph);
96 let mut current = graph.clone();
97 let mut reports = Vec::new();
98 let mut stages = Vec::new();
99 if self.dump {
100 stages.push(("input".to_owned(), current.to_string()));
101 }
102 for pass in &self.passes {
103 let values_before = current.values().len();
104 let output = pass.run(¤t)?;
105 check(
106 &output.graph,
107 &self.limits,
108 &format!("pass `{}` produced an invalid graph", pass.name()),
109 )?;
110 if interface(&output.graph) != expected {
111 return Err(Error::Verify(format!(
112 "pass `{}` changed the graph interface",
113 pass.name()
114 )));
115 }
116 let report = PassReport {
117 pass: pass.name(),
118 values_before,
119 values_after: output.graph.values().len(),
120 changed: output.changed,
121 };
122 if self.dump {
123 stages.push((
124 format!(
125 "after {} (changed {}, values {} -> {})",
126 report.pass, report.changed, report.values_before, report.values_after
127 ),
128 output.graph.to_string(),
129 ));
130 }
131 reports.push(report);
132 current = output.graph;
133 }
134 Ok(Optimized {
135 graph: current,
136 reports,
137 stages,
138 })
139 }
140}
141
142type Interface = (Vec<(String, TensorType)>, Vec<String>, Vec<TensorType>);
145
146fn interface(graph: &Graph) -> Interface {
147 (
148 graph
149 .inputs()
150 .iter()
151 .map(|&id| (graph.label(id), graph.value(id).ty.clone()))
152 .collect(),
153 graph.output_names().to_vec(),
154 graph
155 .outputs()
156 .iter()
157 .map(|&id| graph.value(id).ty.clone())
158 .collect(),
159 )
160}
161
162fn check(graph: &Graph, limits: &Limits, context: &str) -> Result<(), Error> {
163 graph
164 .verify()
165 .and_then(|()| graph.verify_limits(limits))
166 .map_err(|e| Error::Verify(format!("{context}: {e}")))
167}
168
169pub fn default_pipeline(limits: Limits, evaluator: Arc<dyn ConstEvaluator>) -> PassManager {
174 PassManager::new(limits.clone())
175 .add(ConstantFolding::new(evaluator, limits.clone()))
176 .add(CommonSubexpressionElimination::new(limits.clone()))
177 .add(DeadCodeElimination::new(limits))
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::{GraphBuilder, Op};
184
185 fn sample() -> Graph {
186 let mut b = GraphBuilder::new(Limits::default());
187 let x = b.input("x", vec![2]).unwrap();
188 let y = b.node("y", Op::Relu, &[x]).unwrap();
189 b.finish(&[y]).unwrap()
190 }
191
192 struct Identity;
193 impl Pass for Identity {
194 fn name(&self) -> &'static str {
195 "identity"
196 }
197 fn run(&self, graph: &Graph) -> Result<PassOutput, Error> {
198 Ok(PassOutput {
199 graph: graph.clone(),
200 changed: 0,
201 })
202 }
203 }
204
205 struct Breaker;
207 impl Pass for Breaker {
208 fn name(&self) -> &'static str {
209 "breaker"
210 }
211 fn run(&self, graph: &Graph) -> Result<PassOutput, Error> {
212 let mut broken = graph.clone();
213 broken.outputs.clear();
214 broken.output_names.clear();
215 Ok(PassOutput {
216 graph: broken,
217 changed: 1,
218 })
219 }
220 }
221
222 #[test]
223 fn passes_run_in_order_and_are_reported() {
224 let manager = PassManager::new(Limits::default())
225 .add(Identity)
226 .add(Identity);
227 let optimized = manager.run(&sample()).unwrap();
228 assert_eq!(optimized.graph, sample());
229 assert_eq!(
230 optimized.reports,
231 vec![
232 PassReport {
233 pass: "identity",
234 values_before: 2,
235 values_after: 2,
236 changed: 0
237 },
238 PassReport {
239 pass: "identity",
240 values_before: 2,
241 values_after: 2,
242 changed: 0
243 },
244 ]
245 );
246 }
247
248 #[test]
249 fn without_removes_every_pass_with_that_name() {
250 let manager = PassManager::new(Limits::default())
251 .add(Identity)
252 .add(Breaker)
253 .without("breaker");
254 assert_eq!(manager.pass_names(), ["identity"]);
255 assert!(manager.run(&sample()).is_ok());
256 }
257
258 #[test]
259 fn a_pass_that_breaks_the_graph_is_named() {
260 let manager = PassManager::new(Limits::default())
261 .add(Identity)
262 .add(Breaker);
263 let err = manager.run(&sample()).unwrap_err();
264 assert!(
265 matches!(&err, Error::Verify(m) if m.contains("pass `breaker` produced an invalid graph")),
266 "{err}"
267 );
268 }
269
270 struct Renamer;
272 impl Pass for Renamer {
273 fn name(&self) -> &'static str {
274 "renamer"
275 }
276 fn run(&self, graph: &Graph) -> Result<PassOutput, Error> {
277 let mut renamed = graph.clone();
278 renamed.output_names[0] = "renamed".to_owned();
279 Ok(PassOutput {
280 graph: renamed,
281 changed: 1,
282 })
283 }
284 }
285
286 #[test]
287 fn a_pass_that_changes_the_interface_is_rejected() {
288 let manager = PassManager::new(Limits::default()).add(Renamer);
289 let err = manager.run(&sample()).unwrap_err();
290 assert!(
291 matches!(&err, Error::Verify(m) if m.contains("pass `renamer` changed the graph interface")),
292 "{err}"
293 );
294 }
295
296 #[test]
297 fn invalid_input_is_rejected_before_any_pass_runs() {
298 let mut broken = sample();
299 broken.outputs.clear();
300 broken.output_names.clear();
301 let manager = PassManager::new(Limits::default()).add(Identity);
302 let err = manager.run(&broken).unwrap_err();
303 assert!(
304 matches!(&err, Error::Verify(m) if m.contains("input graph is invalid")),
305 "{err}"
306 );
307 }
308
309 #[test]
310 fn an_input_graph_over_the_limits_is_rejected() {
311 let tight = Limits {
312 max_values: 1,
313 ..Limits::default()
314 };
315 let manager = PassManager::new(tight).add(Identity);
316 let err = manager.run(&sample()).unwrap_err();
317 assert!(
318 matches!(&err, Error::Verify(m) if m.contains("input graph is invalid")),
319 "{err}"
320 );
321 }
322
323 #[test]
324 fn dumps_are_recorded_only_when_asked() {
325 let quiet = PassManager::new(Limits::default()).add(Identity);
326 assert!(quiet.run(&sample()).unwrap().stages.is_empty());
327 let loud = PassManager::new(Limits::default())
328 .add(Identity)
329 .with_dumps(true);
330 let optimized = loud.run(&sample()).unwrap();
331 let labels: Vec<&str> = optimized.stages.iter().map(|(l, _)| l.as_str()).collect();
332 assert_eq!(
333 labels,
334 ["input", "after identity (changed 0, values 2 -> 2)"]
335 );
336 let text = optimized.dump_text();
337 assert!(text.starts_with("== input ==\ngraph {\n"), "{text}");
338 assert!(
339 text.contains("== after identity (changed 0, values 2 -> 2) ==\n"),
340 "{text}"
341 );
342 assert!(text.ends_with("}\n"), "{text}");
343 }
344
345 #[test]
346 fn the_default_pipeline_runs_fold_cse_dce_in_that_order() {
347 struct Never;
348 impl ConstEvaluator for Never {
349 fn eval(
350 &self,
351 _: &crate::Op,
352 _: &[(&crate::TensorType, &[f32])],
353 ) -> Result<Vec<f32>, Error> {
354 Err(Error::Verify("unused".into()))
355 }
356 }
357 let manager = default_pipeline(Limits::default(), std::sync::Arc::new(Never));
358 assert_eq!(manager.pass_names(), ["fold", "cse", "dce"]);
359 assert_eq!(manager.without("cse").pass_names(), ["fold", "dce"]);
360 }
361}