tenflowers_core/graph/optimization/
manager.rs1use super::fusion::OperationFusionPass;
7use super::memory::MemoryOptimizationPass;
8use super::passes::OptimizationPass;
9use super::passes::{
10 AlgebraicSimplificationPass, CSEPass, ConstantFoldingPass, DeadCodeEliminationPass,
11 OperationSchedulingPass, StrengthReductionPass,
12};
13use super::placement::DevicePlacementOptimizationPass;
14use crate::graph::{Graph, NodeId};
15use crate::Result;
16use std::collections::{HashMap, HashSet};
17
18pub struct GraphOptimizer {
20 passes: Vec<Box<dyn OptimizationPass>>,
21 max_iterations: usize,
22}
23
24impl GraphOptimizer {
25 pub fn new() -> Self {
27 let mut optimizer = Self::empty();
28 optimizer.add_default_passes();
29 optimizer
30 }
31
32 pub fn empty() -> Self {
34 Self {
35 passes: Vec::new(),
36 max_iterations: 10,
37 }
38 }
39
40 pub fn add_default_passes(&mut self) {
42 self.add_pass(Box::new(ConstantFoldingPass::new())); self.add_pass(Box::new(AlgebraicSimplificationPass::new())); self.add_pass(Box::new(CSEPass::new())); self.add_pass(Box::new(StrengthReductionPass::new())); self.add_pass(Box::new(OperationSchedulingPass::new())); self.add_pass(Box::new(OperationFusionPass::new()));
49 self.add_pass(Box::new(MemoryOptimizationPass::new()));
50 self.add_pass(Box::new(DevicePlacementOptimizationPass::new()));
51 self.add_pass(Box::new(DeadCodeEliminationPass::new())); }
53
54 pub fn for_eager_execution() -> Self {
75 let mut optimizer = Self::empty();
76 optimizer.add_pass(Box::new(ConstantFoldingPass::new()));
77 optimizer.add_pass(Box::new(AlgebraicSimplificationPass::new()));
78 optimizer.add_pass(Box::new(CSEPass::new()));
79 optimizer.add_pass(Box::new(StrengthReductionPass::new()));
80 optimizer.add_pass(Box::new(OperationSchedulingPass::new()));
81 optimizer.add_pass(Box::new(DeadCodeEliminationPass::new()));
82 optimizer
83 }
84
85 pub fn add_pass(&mut self, pass: Box<dyn OptimizationPass>) {
87 self.passes.push(pass);
88 self.passes.sort_by_key(|b| std::cmp::Reverse(b.priority()));
90 }
91
92 pub fn set_max_iterations(&mut self, max_iterations: usize) {
94 self.max_iterations = max_iterations;
95 }
96
97 pub fn pass_count(&self) -> usize {
99 self.passes.len()
100 }
101
102 pub fn optimize(&self, graph: &mut Graph) -> Result<OptimizationStats> {
104 let mut stats = OptimizationStats::new();
105 let mut iteration = 0;
106
107 while iteration < self.max_iterations {
108 let mut changed_in_iteration = false;
109
110 for pass in &self.passes {
111 if pass.is_applicable(graph) {
112 let start_time = std::time::Instant::now();
113 let changed = pass.apply(graph)?;
114 let duration = start_time.elapsed();
115
116 stats.record_pass(pass.name(), duration, changed);
117
118 if changed {
119 changed_in_iteration = true;
120 }
121 }
122 }
123
124 if !changed_in_iteration {
125 break; }
127
128 iteration += 1;
129 stats.iterations += 1;
130 }
131
132 Ok(stats)
133 }
134
135 pub fn optimize_with_outputs(
148 &self,
149 graph: &mut Graph,
150 outputs: &HashSet<NodeId>,
151 ) -> Result<OptimizationStats> {
152 for pass in &self.passes {
153 pass.set_outputs(outputs);
154 }
155 self.optimize(graph)
156 }
157}
158
159impl Default for GraphOptimizer {
160 fn default() -> Self {
161 Self::new()
162 }
163}
164
165#[derive(Debug, Clone)]
167pub struct OptimizationStats {
168 pub iterations: usize,
169 pub total_time: std::time::Duration,
170 pub pass_stats: HashMap<String, PassStats>,
171}
172
173#[derive(Debug, Clone)]
174pub struct PassStats {
175 pub runs: usize,
176 pub changes: usize,
177 pub total_time: std::time::Duration,
178}
179
180impl OptimizationStats {
181 fn new() -> Self {
182 Self {
183 iterations: 0,
184 total_time: std::time::Duration::new(0, 0),
185 pass_stats: HashMap::new(),
186 }
187 }
188
189 fn record_pass(&mut self, pass_name: &str, duration: std::time::Duration, changed: bool) {
190 self.total_time += duration;
191
192 let stats = self
193 .pass_stats
194 .entry(pass_name.to_string())
195 .or_insert(PassStats {
196 runs: 0,
197 changes: 0,
198 total_time: std::time::Duration::new(0, 0),
199 });
200
201 stats.runs += 1;
202 stats.total_time += duration;
203 if changed {
204 stats.changes += 1;
205 }
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_graph_optimizer_creation() {
215 let optimizer = GraphOptimizer::new();
216 assert_eq!(optimizer.passes.len(), 9); let empty_optimizer = GraphOptimizer::empty();
219 assert_eq!(empty_optimizer.passes.len(), 0);
220 }
221
222 #[test]
223 fn test_optimization_stats() {
224 let mut stats = OptimizationStats::new();
225 assert_eq!(stats.iterations, 0);
226 assert_eq!(stats.pass_stats.len(), 0);
227
228 stats.record_pass("test_pass", std::time::Duration::from_millis(10), true);
229 assert_eq!(stats.pass_stats.len(), 1);
230 assert!(stats.pass_stats.contains_key("test_pass"));
231 }
232
233 #[test]
234 fn test_pass_addition() {
235 let mut optimizer = GraphOptimizer::empty();
236
237 optimizer.add_pass(Box::new(ConstantFoldingPass::new()));
239 assert_eq!(optimizer.passes.len(), 1);
240
241 optimizer.add_pass(Box::new(DeadCodeEliminationPass::new()));
243 assert_eq!(optimizer.passes.len(), 2);
244
245 assert!(optimizer.passes[0].priority() >= optimizer.passes[1].priority());
247 }
248
249 #[test]
250 fn test_max_iterations() {
251 let mut optimizer = GraphOptimizer::new();
252 optimizer.set_max_iterations(5);
253 assert_eq!(optimizer.max_iterations, 5);
254 }
255
256 #[test]
257 fn test_for_eager_execution_excludes_unsupported_passes() {
258 let eager = GraphOptimizer::for_eager_execution();
264 assert_eq!(eager.pass_count(), 6);
265
266 let default_optimizer = GraphOptimizer::new();
267 assert_eq!(default_optimizer.pass_count(), 9);
268
269 for pass in &eager.passes {
270 assert_ne!(pass.name(), "OperationFusion");
271 assert_ne!(pass.name(), "MemoryOptimization");
272 assert_ne!(pass.name(), "DevicePlacementOptimization");
273 }
274 }
275
276 #[test]
277 fn test_optimize_with_outputs_protects_marked_node() {
278 let mut graph = Graph::new();
281 let x = graph
282 .add_node(
283 "x".to_string(),
284 crate::graph::NodeType::Placeholder {
285 dtype: crate::dtype::DType::Float32,
286 shape: crate::shape::Shape::new(vec![]),
287 },
288 crate::device::Device::Cpu,
289 HashMap::new(),
290 )
291 .expect("test: add_node should succeed");
292 let mut zero_attrs = HashMap::new();
293 zero_attrs.insert(
294 "value".to_string(),
295 crate::graph::AttributeValue::Tensor(crate::tensor::Tensor::from_scalar(0.0f32)),
296 );
297 let zero = graph
298 .add_node(
299 "zero".to_string(),
300 crate::graph::NodeType::Constant,
301 crate::device::Device::Cpu,
302 zero_attrs,
303 )
304 .expect("test: add_node should succeed");
305 let add = graph
306 .add_node(
307 "x_plus_zero".to_string(),
308 crate::graph::NodeType::Operation("Add".to_string()),
309 crate::device::Device::Cpu,
310 HashMap::new(),
311 )
312 .expect("test: add_node should succeed");
313 graph
314 .add_edge(
315 x,
316 add,
317 0,
318 0,
319 crate::dtype::DType::Float32,
320 crate::shape::Shape::new(vec![]),
321 false,
322 )
323 .expect("test: add_edge should succeed");
324 graph
325 .add_edge(
326 zero,
327 add,
328 0,
329 1,
330 crate::dtype::DType::Float32,
331 crate::shape::Shape::new(vec![]),
332 false,
333 )
334 .expect("test: add_edge should succeed");
335
336 let optimizer = GraphOptimizer::for_eager_execution();
337 let mut outputs = HashSet::new();
338 outputs.insert(add);
339 optimizer
340 .optimize_with_outputs(&mut graph, &outputs)
341 .expect("test: optimize_with_outputs should succeed");
342
343 assert!(
344 graph.get_node(add).is_some(),
345 "the marked output node must survive optimization"
346 );
347 }
348
349 #[test]
350 fn test_pass_stats_recording() {
351 let mut stats = OptimizationStats::new();
352
353 stats.record_pass("pass1", std::time::Duration::from_millis(10), true);
355 stats.record_pass("pass1", std::time::Duration::from_millis(5), false);
356 stats.record_pass("pass2", std::time::Duration::from_millis(20), true);
357
358 assert_eq!(stats.pass_stats.len(), 2);
360
361 let pass1_stats = &stats.pass_stats["pass1"];
362 assert_eq!(pass1_stats.runs, 2);
363 assert_eq!(pass1_stats.changes, 1);
364 assert_eq!(pass1_stats.total_time, std::time::Duration::from_millis(15));
365
366 let pass2_stats = &stats.pass_stats["pass2"];
367 assert_eq!(pass2_stats.runs, 1);
368 assert_eq!(pass2_stats.changes, 1);
369 assert_eq!(pass2_stats.total_time, std::time::Duration::from_millis(20));
370
371 assert_eq!(stats.total_time, std::time::Duration::from_millis(35));
373 }
374}