1use crate::{
10 graph::{Graph, GraphNode, NodeId, NodeType},
11 Result,
12};
13#[cfg(feature = "serialize")]
14use serde::{Deserialize, Serialize};
15use std::collections::HashSet;
16
17#[derive(Debug, Clone)]
19#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
20pub struct GraphFreezingConfig {
21 pub remove_training_ops: bool,
23 pub fold_constants: bool,
25 pub remove_dead_code: bool,
27 pub optimize_sequential_ops: bool,
29 pub target_device: String,
31}
32
33impl Default for GraphFreezingConfig {
34 fn default() -> Self {
35 Self {
36 remove_training_ops: true,
37 fold_constants: true,
38 remove_dead_code: true,
39 optimize_sequential_ops: true,
40 target_device: "cpu".to_string(),
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
47#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
48pub struct GraphFreezingStats {
49 pub nodes_before: usize,
51 pub nodes_after: usize,
53 pub constants_folded: usize,
55 pub training_ops_removed: usize,
57 pub dead_nodes_removed: usize,
59 pub memory_savings: usize,
61}
62
63pub struct GraphFreezer {
65 config: GraphFreezingConfig,
66}
67
68impl GraphFreezer {
69 pub fn new() -> Self {
71 Self {
72 config: GraphFreezingConfig::default(),
73 }
74 }
75
76 pub fn with_config(config: GraphFreezingConfig) -> Self {
78 Self { config }
79 }
80
81 pub fn freeze_graph(&self, graph: &Graph) -> Result<(Graph, GraphFreezingStats)> {
83 let mut frozen_graph = graph.clone();
84 let mut stats = GraphFreezingStats {
85 nodes_before: frozen_graph.node_count(),
86 nodes_after: 0,
87 constants_folded: 0,
88 training_ops_removed: 0,
89 dead_nodes_removed: 0,
90 memory_savings: 0,
91 };
92
93 if self.config.remove_training_ops {
95 self.remove_training_operations(&mut frozen_graph, &mut stats)?;
96 }
97
98 if self.config.fold_constants {
100 self.fold_constants(&mut frozen_graph, &mut stats)?;
101 }
102
103 if self.config.remove_dead_code {
105 self.remove_dead_code(&mut frozen_graph, &mut stats)?;
106 }
107
108 if self.config.optimize_sequential_ops {
110 self.optimize_sequential_operations(&mut frozen_graph, &mut stats)?;
111 }
112
113 stats.nodes_after = frozen_graph.node_count();
114 stats.memory_savings = self.estimate_memory_savings(&stats);
115
116 Ok((frozen_graph, stats))
117 }
118
119 fn remove_training_operations(
121 &self,
122 graph: &mut Graph,
123 stats: &mut GraphFreezingStats,
124 ) -> Result<()> {
125 let training_ops = ["Dropout", "BatchNorm", "GroupNorm", "LayerNorm"];
126 let mut ops_to_remove = Vec::new();
127
128 for node in graph.nodes() {
130 if let NodeType::Operation(op_name) = &node.op_type {
131 if training_ops.contains(&op_name.as_str()) {
132 if self.is_training_mode_operation(node) {
134 ops_to_remove.push(node.id);
135 }
136 }
137 }
138 }
139
140 for node_id in ops_to_remove {
142 self.replace_training_operation(graph, node_id)?;
143 stats.training_ops_removed += 1;
144 }
145
146 Ok(())
147 }
148
149 fn is_training_mode_operation(&self, node: &GraphNode) -> bool {
151 if let Some(training_attr) = node.attributes.get("training") {
153 match training_attr {
154 crate::graph::AttributeValue::Bool(training) => *training,
155 crate::graph::AttributeValue::String(s) => s.parse::<bool>().unwrap_or(false),
156 _ => false,
157 }
158 } else {
159 false
161 }
162 }
163
164 fn replace_training_operation(&self, graph: &mut Graph, node_id: NodeId) -> Result<()> {
166 if let Some(node) = graph.get_node(node_id) {
167 if let NodeType::Operation(op_name) = &node.op_type {
168 match op_name.as_str() {
169 "Dropout" => {
170 self.replace_with_identity(graph, node_id)?;
172 }
173 "BatchNorm" | "GroupNorm" | "LayerNorm" => {
174 self.convert_normalization_to_inference(graph, node_id)?;
176 }
177 _ => {
178 }
180 }
181 }
182 }
183 Ok(())
184 }
185
186 fn replace_with_identity(&self, _graph: &mut Graph, _node_id: NodeId) -> Result<()> {
188 Ok(())
195 }
196
197 fn convert_normalization_to_inference(
199 &self,
200 _graph: &mut Graph,
201 _node_id: NodeId,
202 ) -> Result<()> {
203 Ok(())
209 }
210
211 fn fold_constants(&self, graph: &mut Graph, stats: &mut GraphFreezingStats) -> Result<()> {
213 let mut constants_folded = 0;
214 let mut nodes_to_process = Vec::new();
215
216 for node in graph.nodes() {
218 if self.can_fold_constant(graph, node.id) {
219 nodes_to_process.push(node.id);
220 }
221 }
222
223 for node_id in nodes_to_process {
225 if self.fold_constant_node(graph, node_id)? {
226 constants_folded += 1;
227 }
228 }
229
230 stats.constants_folded = constants_folded;
231 Ok(())
232 }
233
234 fn can_fold_constant(&self, graph: &Graph, node_id: NodeId) -> bool {
236 let mut all_inputs_constant = true;
238
239 if let Some(node) = graph.get_node(node_id) {
240 for edge_id in &node.inputs {
241 if let Some(edge) = graph.get_edge(*edge_id) {
242 if let Some(source_node) = graph.get_node(edge.from_node) {
243 if !self.is_constant_node(source_node) {
244 all_inputs_constant = false;
245 break;
246 }
247 }
248 }
249 }
250 }
251
252 all_inputs_constant && self.is_deterministic_operation(graph, node_id)
253 }
254
255 fn is_constant_node(&self, node: &crate::graph::GraphNode) -> bool {
257 if let NodeType::Constant = node.op_type {
258 true
259 } else if let NodeType::Operation(op_name) = &node.op_type {
260 matches!(op_name.as_str(), "Const" | "Constant" | "Identity")
261 } else {
262 false
263 }
264 }
265
266 fn is_deterministic_operation(&self, graph: &Graph, node_id: NodeId) -> bool {
268 if let Some(node) = graph.get_node(node_id) {
269 if let NodeType::Operation(op_name) = &node.op_type {
271 !matches!(
272 op_name.as_str(),
273 "Random" | "RandomNormal" | "RandomUniform" | "Dropout"
274 )
275 } else {
276 true
277 }
278 } else {
279 false
280 }
281 }
282
283 fn fold_constant_node(&self, _graph: &mut Graph, _node_id: NodeId) -> Result<bool> {
285 Ok(true)
293 }
294
295 fn remove_dead_code(&self, graph: &mut Graph, stats: &mut GraphFreezingStats) -> Result<()> {
297 let mut reachable_nodes = HashSet::new();
298 let mut nodes_to_remove = Vec::new();
299
300 self.mark_reachable_nodes(graph, &mut reachable_nodes)?;
302
303 for node in graph.nodes() {
305 if !reachable_nodes.contains(&node.id) {
306 nodes_to_remove.push(node.id);
307 }
308 }
309
310 for node_id in nodes_to_remove {
312 let _ = graph.remove_node(node_id);
313 stats.dead_nodes_removed += 1;
314 }
315
316 Ok(())
317 }
318
319 fn mark_reachable_nodes(&self, graph: &Graph, reachable: &mut HashSet<NodeId>) -> Result<()> {
321 let output_nodes = self.find_output_nodes(graph);
323
324 for output_id in output_nodes {
325 self.mark_reachable_recursive(graph, output_id, reachable);
326 }
327
328 Ok(())
329 }
330
331 fn find_output_nodes(&self, graph: &Graph) -> Vec<NodeId> {
333 let mut output_nodes = Vec::new();
334
335 for node in graph.nodes() {
336 if node.outputs.is_empty() {
338 output_nodes.push(node.id);
339 }
340 }
341
342 output_nodes
343 }
344
345 #[allow(clippy::only_used_in_recursion)]
347 fn mark_reachable_recursive(
348 &self,
349 graph: &Graph,
350 node_id: NodeId,
351 reachable: &mut HashSet<NodeId>,
352 ) {
353 if reachable.contains(&node_id) {
354 return;
355 }
356
357 reachable.insert(node_id);
358
359 if let Some(node) = graph.get_node(node_id) {
361 for edge_id in &node.inputs {
362 if let Some(edge) = graph.get_edge(*edge_id) {
363 self.mark_reachable_recursive(graph, edge.from_node, reachable);
364 }
365 }
366 }
367 }
368
369 fn optimize_sequential_operations(
371 &self,
372 graph: &mut Graph,
373 stats: &mut GraphFreezingStats,
374 ) -> Result<()> {
375 let mut _optimizations = 0;
379
380 _optimizations += self.fuse_consecutive_reshapes(graph)?;
382
383 _optimizations += self.fuse_linear_operations(graph)?;
385
386 stats.nodes_after = graph.node_count();
388
389 Ok(())
390 }
391
392 fn fuse_consecutive_reshapes(&self, graph: &mut Graph) -> Result<usize> {
394 let mut fused_count = 0;
395
396 let mut nodes_to_process = Vec::new();
398
399 for node in graph.nodes() {
400 if let NodeType::Operation(op_name) = &node.op_type {
401 if op_name == "Reshape" {
402 nodes_to_process.push(node.id);
403 }
404 }
405 }
406
407 for node_id in nodes_to_process {
409 if self.can_fuse_reshape_chain(graph, node_id) {
410 self.fuse_reshape_chain(graph, node_id)?;
411 fused_count += 1;
412 }
413 }
414
415 Ok(fused_count)
416 }
417
418 fn can_fuse_reshape_chain(&self, graph: &Graph, node_id: NodeId) -> bool {
420 if let Some(node) = graph.get_node(node_id) {
422 for edge_id in &node.outputs {
423 if let Some(edge) = graph.get_edge(*edge_id) {
424 if let Some(target_node) = graph.get_node(edge.to_node) {
425 if let NodeType::Operation(op_name) = &target_node.op_type {
426 if op_name == "Reshape" {
427 return true;
428 }
429 }
430 }
431 }
432 }
433 }
434 false
435 }
436
437 fn fuse_reshape_chain(&self, _graph: &mut Graph, _node_id: NodeId) -> Result<()> {
439 Ok(())
444 }
445
446 fn fuse_linear_operations(&self, graph: &mut Graph) -> Result<usize> {
448 let mut fused_count = 0;
449
450 let linear_ops = ["Add", "Multiply", "Subtract", "Divide"];
452
453 let node_ids: Vec<_> = graph
455 .nodes()
456 .filter_map(|node| {
457 if let NodeType::Operation(op_name) = &node.op_type {
458 if linear_ops.contains(&op_name.as_str()) {
459 Some(node.id)
460 } else {
461 None
462 }
463 } else {
464 None
465 }
466 })
467 .collect();
468
469 for node_id in node_ids {
470 if self.can_fuse_linear_operation(graph, node_id) {
471 self.fuse_linear_operation(graph, node_id)?;
472 fused_count += 1;
473 }
474 }
475
476 Ok(fused_count)
477 }
478
479 fn can_fuse_linear_operation(&self, graph: &Graph, node_id: NodeId) -> bool {
481 if let Some(node) = graph.get_node(node_id) {
488 for edge_id in &node.outputs {
489 if let Some(edge) = graph.get_edge(*edge_id) {
490 if let Some(target_node) = graph.get_node(edge.to_node) {
491 if let NodeType::Operation(op_name) = &target_node.op_type {
492 if matches!(op_name.as_str(), "Add" | "Multiply") {
493 return true;
494 }
495 }
496 }
497 }
498 }
499 }
500 false
501 }
502
503 fn fuse_linear_operation(&self, _graph: &mut Graph, _node_id: NodeId) -> Result<()> {
505 Ok(())
508 }
509
510 fn estimate_memory_savings(&self, stats: &GraphFreezingStats) -> usize {
512 let nodes_removed = stats.training_ops_removed + stats.dead_nodes_removed;
513 let constants_folded = stats.constants_folded;
514
515 (nodes_removed * 1024) + (constants_folded * 512)
517 }
518}
519
520impl Default for GraphFreezer {
521 fn default() -> Self {
522 Self::new()
523 }
524}
525
526pub fn freeze_graph_for_inference(graph: &Graph) -> Result<(Graph, GraphFreezingStats)> {
528 let freezer = GraphFreezer::new();
529 freezer.freeze_graph(graph)
530}
531
532pub fn freeze_graph_with_config(
534 graph: &Graph,
535 config: GraphFreezingConfig,
536) -> Result<(Graph, GraphFreezingStats)> {
537 let freezer = GraphFreezer::with_config(config);
538 freezer.freeze_graph(graph)
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544 use crate::graph::Graph;
545
546 #[test]
547 fn test_graph_freezer_creation() {
548 let freezer = GraphFreezer::new();
549 assert!(freezer.config.remove_training_ops);
550 assert!(freezer.config.fold_constants);
551 assert!(freezer.config.remove_dead_code);
552 }
553
554 #[test]
555 fn test_graph_freezer_with_config() {
556 let config = GraphFreezingConfig {
557 remove_training_ops: false,
558 fold_constants: true,
559 remove_dead_code: true,
560 optimize_sequential_ops: false,
561 target_device: "gpu".to_string(),
562 };
563
564 let freezer = GraphFreezer::with_config(config.clone());
565 assert_eq!(
566 freezer.config.remove_training_ops,
567 config.remove_training_ops
568 );
569 assert_eq!(freezer.config.target_device, config.target_device);
570 }
571
572 #[test]
573 fn test_freeze_empty_graph() {
574 let graph = Graph::new();
575 let result = freeze_graph_for_inference(&graph);
576 assert!(result.is_ok());
577
578 let (frozen_graph, stats) = result.expect("test: operation should succeed");
579 assert_eq!(stats.nodes_before, 0);
580 assert_eq!(stats.nodes_after, 0);
581 }
582
583 #[test]
584 fn test_memory_savings_estimation() {
585 let freezer = GraphFreezer::new();
586 let stats = GraphFreezingStats {
587 nodes_before: 100,
588 nodes_after: 80,
589 constants_folded: 10,
590 training_ops_removed: 5,
591 dead_nodes_removed: 5,
592 memory_savings: 0,
593 };
594
595 let estimated_savings = freezer.estimate_memory_savings(&stats);
596 assert_eq!(estimated_savings, (10 * 1024) + (10 * 512)); }
598}