Skip to main content

tenflowers_core/graph/optimization/
fusion.rs

1//! Operation fusion optimization pass
2//!
3//! This module provides operation fusion capabilities that combine multiple
4//! operations into fused kernels for improved performance.
5
6use super::passes::{get_node_outputs, OptimizationPass};
7use crate::graph::{Graph, NodeId};
8use crate::Result;
9
10/// Operation fusion pass
11/// Combines multiple operations into fused kernels
12pub struct OperationFusionPass;
13
14impl OptimizationPass for OperationFusionPass {
15    fn apply(&self, graph: &mut Graph) -> Result<bool> {
16        let mut changed = false;
17        let mut fusion_candidates = Vec::new();
18
19        // Look for fusable patterns
20        for node in graph.nodes() {
21            if let crate::graph::NodeType::Operation(op_name) = &node.op_type {
22                match op_name.as_str() {
23                    "MatMul" => {
24                        // Look for MatMul + Add (bias) pattern
25                        if let Some(add_node) = self.find_add_after_matmul(graph, node.id) {
26                            fusion_candidates.push(FusionCandidate {
27                                pattern: FusionPattern::MatMulAdd,
28                                nodes: vec![node.id, add_node],
29                            });
30                            changed = true;
31                        }
32                    }
33                    "Add" => {
34                        // Look for Add + ReLU pattern
35                        if let Some(relu_node) = self.find_activation_after_add(graph, node.id) {
36                            fusion_candidates.push(FusionCandidate {
37                                pattern: FusionPattern::AddActivation,
38                                nodes: vec![node.id, relu_node],
39                            });
40                            changed = true;
41                        }
42                    }
43                    "Conv2D" => {
44                        // Look for Conv + BatchNorm + ReLU pattern
45                        if let Some(fusion) = self.find_conv_bn_relu_pattern(graph, node.id) {
46                            fusion_candidates.push(fusion);
47                            changed = true;
48                        }
49                    }
50                    _ => {}
51                }
52            }
53        }
54
55        // Apply fusions
56        for candidate in fusion_candidates {
57            self.apply_fusion(graph, &candidate)?;
58        }
59
60        Ok(changed)
61    }
62
63    fn name(&self) -> &str {
64        "OperationFusion"
65    }
66
67    fn is_applicable(&self, graph: &Graph) -> bool {
68        graph.node_count() > 1
69    }
70
71    fn priority(&self) -> u32 {
72        120 // Medium priority - after CSE but before layout optimization
73    }
74}
75
76impl Default for OperationFusionPass {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl OperationFusionPass {
83    pub fn new() -> Self {
84        Self
85    }
86
87    fn find_add_after_matmul(&self, graph: &Graph, matmul_node: NodeId) -> Option<NodeId> {
88        let outputs = get_node_outputs(graph, matmul_node);
89        for output_id in outputs {
90            if let Some(node) = graph.get_node(output_id) {
91                if let crate::graph::NodeType::Operation(op_name) = &node.op_type {
92                    if op_name == "Add" {
93                        return Some(output_id);
94                    }
95                }
96            }
97        }
98        None
99    }
100
101    fn find_activation_after_add(&self, graph: &Graph, add_node: NodeId) -> Option<NodeId> {
102        let outputs = get_node_outputs(graph, add_node);
103        for output_id in outputs {
104            if let Some(node) = graph.get_node(output_id) {
105                if let crate::graph::NodeType::Operation(op_name) = &node.op_type {
106                    if matches!(op_name.as_str(), "ReLU" | "Sigmoid" | "Tanh") {
107                        return Some(output_id);
108                    }
109                }
110            }
111        }
112        None
113    }
114
115    fn find_conv_bn_relu_pattern(
116        &self,
117        graph: &Graph,
118        conv_node: NodeId,
119    ) -> Option<FusionCandidate> {
120        let mut current_node = conv_node;
121        let mut pattern_nodes = vec![current_node];
122
123        // Look for BatchNorm after Conv
124        if let Some(bn_node) = self.find_next_node_with_type(graph, current_node, "BatchNorm") {
125            pattern_nodes.push(bn_node);
126            current_node = bn_node;
127
128            // Look for ReLU after BatchNorm
129            if let Some(relu_node) = self.find_next_node_with_type(graph, current_node, "ReLU") {
130                pattern_nodes.push(relu_node);
131
132                return Some(FusionCandidate {
133                    pattern: FusionPattern::ConvBatchNormReLU,
134                    nodes: pattern_nodes,
135                });
136            }
137        }
138
139        None
140    }
141
142    fn find_next_node_with_type(
143        &self,
144        graph: &Graph,
145        node_id: NodeId,
146        op_type: &str,
147    ) -> Option<NodeId> {
148        let outputs = get_node_outputs(graph, node_id);
149        for output_id in outputs {
150            if let Some(node) = graph.get_node(output_id) {
151                if let crate::graph::NodeType::Operation(op_name) = &node.op_type {
152                    if op_name == op_type {
153                        return Some(output_id);
154                    }
155                }
156            }
157        }
158        None
159    }
160
161    fn apply_fusion(&self, graph: &mut Graph, candidate: &FusionCandidate) -> Result<()> {
162        match candidate.pattern {
163            FusionPattern::MatMulAdd => self.fuse_matmul_add(graph, &candidate.nodes),
164            FusionPattern::AddActivation => self.fuse_add_activation(graph, &candidate.nodes),
165            FusionPattern::ConvBatchNormReLU => self.fuse_conv_bn_relu(graph, &candidate.nodes),
166        }
167    }
168
169    fn fuse_matmul_add(&self, graph: &mut Graph, nodes: &[NodeId]) -> Result<()> {
170        if nodes.len() != 2 {
171            return Ok(());
172        }
173
174        let matmul_node = nodes[0];
175        let add_node = nodes[1];
176
177        // Replace the MatMul node with a Dense operation that includes bias
178        if let Some(matmul_node_ref) = graph.get_node_mut(matmul_node) {
179            matmul_node_ref.op_type = crate::graph::NodeType::Operation("Dense".to_string());
180            // In a real implementation, we'd also update the attributes to include bias handling
181        }
182
183        // Remove the Add node and redirect its outputs to the MatMul node
184        graph.redirect_node_outputs(add_node, matmul_node)?;
185        graph.remove_node(add_node)?;
186
187        Ok(())
188    }
189
190    fn fuse_add_activation(&self, graph: &mut Graph, nodes: &[NodeId]) -> Result<()> {
191        if nodes.len() != 2 {
192            return Ok(());
193        }
194
195        let add_node = nodes[0];
196        let activation_node = nodes[1];
197
198        // Get the activation type
199        let activation_type = if let Some(node) = graph.get_node(activation_node) {
200            if let crate::graph::NodeType::Operation(op_name) = &node.op_type {
201                op_name.clone()
202            } else {
203                return Ok(());
204            }
205        } else {
206            return Ok(());
207        };
208
209        // Replace the Add node with a fused Add+Activation operation
210        if let Some(add_node_ref) = graph.get_node_mut(add_node) {
211            add_node_ref.op_type =
212                crate::graph::NodeType::Operation(format!("Add{activation_type}"));
213        }
214
215        // Remove the activation node and redirect its outputs to the add node
216        graph.redirect_node_outputs(activation_node, add_node)?;
217        graph.remove_node(activation_node)?;
218
219        Ok(())
220    }
221
222    fn fuse_conv_bn_relu(&self, graph: &mut Graph, nodes: &[NodeId]) -> Result<()> {
223        if nodes.len() != 3 {
224            return Ok(());
225        }
226
227        let conv_node = nodes[0];
228        let bn_node = nodes[1];
229        let relu_node = nodes[2];
230
231        // Replace the Conv node with a fused ConvBatchNormReLU operation
232        if let Some(conv_node_ref) = graph.get_node_mut(conv_node) {
233            conv_node_ref.op_type =
234                crate::graph::NodeType::Operation("ConvBatchNormReLU".to_string());
235            // In a real implementation, we'd merge the BatchNorm parameters into the conv attributes
236        }
237
238        // Remove the BatchNorm and ReLU nodes
239        graph.redirect_node_outputs(relu_node, conv_node)?;
240        graph.remove_node(bn_node)?;
241        graph.remove_node(relu_node)?;
242
243        Ok(())
244    }
245}
246
247/// Fusion patterns that can be optimized
248#[derive(Debug, Clone)]
249pub enum FusionPattern {
250    MatMulAdd,         // MatMul + Add -> Dense layer
251    AddActivation,     // Add + Activation -> Fused add+activation
252    ConvBatchNormReLU, // Conv + BatchNorm + ReLU -> Fused conv block
253}
254
255/// Candidate for operation fusion
256#[derive(Debug, Clone)]
257#[allow(dead_code)]
258pub struct FusionCandidate {
259    pub pattern: FusionPattern,
260    pub nodes: Vec<NodeId>,
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_operation_fusion_pass() {
269        let pass = OperationFusionPass::new();
270        assert_eq!(pass.name(), "OperationFusion");
271        assert_eq!(pass.priority(), 120);
272    }
273
274    #[test]
275    fn test_fusion_patterns() {
276        // Test that fusion patterns can be created
277        let pattern1 = FusionPattern::MatMulAdd;
278        let _pattern2 = FusionPattern::AddActivation;
279        let _pattern3 = FusionPattern::ConvBatchNormReLU;
280
281        // Test fusion candidate creation
282        let candidate = FusionCandidate {
283            pattern: pattern1,
284            nodes: vec![1, 2],
285        };
286
287        assert_eq!(candidate.nodes.len(), 2);
288    }
289}