tenflowers_core/graph/optimization/
fusion.rs1use super::passes::{get_node_outputs, OptimizationPass};
7use crate::graph::{Graph, NodeId};
8use crate::Result;
9
10pub 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 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 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 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 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 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 }
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 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 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 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 }
182
183 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 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 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 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 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 }
237
238 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#[derive(Debug, Clone)]
249pub enum FusionPattern {
250 MatMulAdd, AddActivation, ConvBatchNormReLU, }
254
255#[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 let pattern1 = FusionPattern::MatMulAdd;
278 let _pattern2 = FusionPattern::AddActivation;
279 let _pattern3 = FusionPattern::ConvBatchNormReLU;
280
281 let candidate = FusionCandidate {
283 pattern: pattern1,
284 nodes: vec![1, 2],
285 };
286
287 assert_eq!(candidate.nodes.len(), 2);
288 }
289}